1 ################################################################################
2 # id3.rb Ruby Module for handling the following ID3-tag versions:
3 # ID3v1.0 , ID3v1.1, ID3v2.2.0, ID3v2.3.0, ID3v2.4.0
5 # Copyright (C) 2002,2003,2004 by Tilo Sloboda <tilo@unixgods.org>
8 # updated: Time-stamp: <Mon 27-Dec-2004 22:23:49 Tilo Sloboda>
10 # Docs: http://www.id3.org/id3v2-00.txt
11 # http://www.id3.org/id3v2.3.0.txt
12 # http://www.id3.org/id3v2.4.0-changes.txt
13 # http://www.id3.org/id3v2.4.0-structure.txt
14 # http://www.id3.org/id3v2.4.0-frames.txt
16 # different versions of ID3 tags, support different fields.
17 # See: http://www.unixgods.org/~tilo/ID3v2_frames_comparison.txt
18 # See: http://www.unixgods.org/~tilo/ID3/docs/ID3_comparison.html
21 # Freely available under the terms of the OpenSource "Artistic License"
22 # in combination with the Addendum A (below)
24 # In case you did not get a copy of the license along with the software,
25 # it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
28 # THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
29 # SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
30 # REPAIR OR CORRECTION.
32 # IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
33 # SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
34 # TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
35 # INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
36 # TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
37 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
40 # The author of this ID3-library for Ruby is not responsible in any way for
41 # the definition of the ID3-standards..
43 # You're lucky though that you can use this little library, rather than having
44 # to parse ID3v2 tags yourself! Trust me! At the first glance it doesn't seem
45 # to be so complicated, but the ID3v2 definitions are so convoluted and
46 # unnecessarily complicated, with so many useless frame-types, it's a pain to
47 # read the documents describing the ID3 V2.x standards.. and even worse
50 # I don't know what these people were thinking... can we make it any more
51 # complicated than that?? ID3 version 2.4.0 tops everything! If this flag
52 # is set and it's a full moon, and an even weekday number, then do this..
53 # Outch!!! I assume that's why I don't find any 2.4.0 tags in any of my
54 # MP3-files... seems like noone is writing 2.4.0 tags... iTunes writes 2.3.0
56 # If you have some files with valid 2.4.0 tags, please send them my way!
59 #-------------------------------------------------------------------------------
63 # hasID3v1tag?(filename)
64 # hasID3v2tag?(filename)
65 # removeID3v1tag(filename)
73 ################################################################################
75 # ==============================================================================
76 # Lading other stuff..
77 # ==============================================================================
81 require 'hexdump' # load hexdump method to extend class String
82 require 'invert_hash' # new invert method for old Hash
85 class Hash # overwrite Hash.invert method
86 alias old_invert invert
96 # ----------------------------------------------------------------------------
98 # ----------------------------------------------------------------------------
99 @@RCSid = '$Id: id3.rb,v 1.2 2004/11/29 05:18:44 tilo Exp tilo $'
101 ID3v1tagSize = 128 # ID3v1 and ID3v1.1 have fixed size tags
102 ID3v1versionbyte = 125
106 SUPPORTED_SYMBOLS = {
107 "1.0" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
108 "YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
111 "1.1" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
112 "YEAR"=>93..96 , "COMMENT"=>97..124,
113 "TRACKNUM"=>126, "GENREID"=>127,
117 "2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
118 "ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
119 "COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
120 "ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC",
121 "DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
122 "ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT",
123 "COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN",
124 "ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
125 "PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
126 "ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
128 "WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
129 "WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
130 "WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
131 "INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
132 "CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
133 "SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
134 "EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
135 "GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
136 "BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
140 "2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
141 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
142 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
143 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
144 "DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
145 "ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ",
146 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
147 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
148 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
149 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
150 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
151 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
152 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
153 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
154 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
155 "INVOLVEDPEOPLE"=>"IPLS",
156 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
157 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
158 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
159 "VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA",
160 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
161 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
162 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
163 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
167 "2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
168 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
169 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
170 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
171 "RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
172 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
173 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
174 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
175 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
176 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
177 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
178 "SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
179 "ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
180 "ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
181 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
182 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
183 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
184 "MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
185 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
186 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
187 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
188 "VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
189 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
190 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
191 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
192 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
194 "OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
195 "AUDIOSEEKPOINT"=>"ASPI"
199 # ----------------------------------------------------------------------------
200 # Flags in the ID3-Tag Header:
202 TAG_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
203 # those flags are supposed to be zero!
204 "2.2.0" => 0x3F, # 0xC0 ,
205 "2.3.0" => 0x1F, # 0xE0 ,
206 "2.4.0" => 0x0F # 0xF0
210 "2.2.0" => { "Unsynchronisation" => 0x80 ,
211 "Compression" => 0x40 ,
213 "2.3.0" => { "Unsynchronisation" => 0x80 ,
214 "ExtendedHeader" => 0x40 ,
215 "Experimental" => 0x20 ,
217 "2.4.0" => { "Unsynchronisation" => 0x80 ,
218 "ExtendedHeader" => 0x40 ,
219 "Experimental" => 0x20 ,
224 # ----------------------------------------------------------------------------
225 # Flags in the ID3-Frame Header:
227 FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
228 # those flags are supposed to be zero!
229 "2.3.0" => 0x1F1F, # 0xD0D0 ,
230 "2.4.0" => 0x8FB0 # 0x704F ,
233 FRAME_HEADER_FLAGS = {
234 "2.3.0" => { "TagAlterPreservation" => 0x8000 ,
235 "FileAlterPreservation" => 0x4000 ,
236 "ReadOnly" => 0x2000 ,
238 "Compression" => 0x0080 ,
239 "Encryption" => 0x0040 ,
240 "GroupIdentity" => 0x0020 ,
242 "2.4.0" => { "TagAlterPreservation" => 0x4000 ,
243 "FileAlterPreservation" => 0x2000 ,
244 "ReadOnly" => 0x1000 ,
246 "GroupIdentity" => 0x0040 ,
247 "Compression" => 0x0008 ,
248 "Encryption" => 0x0004 ,
249 "Unsynchronisation" => 0x0002 ,
250 "DataLengthIndicator" => 0x0001 ,
254 # the FrameTypes are not visible to the user - they are just a mechanism
255 # to define only one parser for multiple FraneNames..
258 FRAMETYPE2FRAMENAME = {
259 "TEXT" => %w(TENTGROUP TITLE SUBTITLE ARTIST BAND CONDUCTOR MIXARTIST COMPOSER LYRICIST LANGUAGE CONTENTTYPE ALBUM TRACKNUM PARTINSET ISRC DATE YEAR TIME RECORDINGDATES ORIGYEAR BPM MEDIATYPE FILETYPE COPYRIGHT PUBLISHER ENCODEDBY ENCODERSETTINGS SONGLEN SIZE PLAYLISTDELAY INITIALKEY ORIGALBUM ORIGFILENAME ORIGARTIST ORIGLYRICIST FILEOWNER NETRADIOSTATION NETRADIOOWNER SETSUBTITLE MOOD PRODUCEDNOTICE ALBUMSORTORDER PERFORMERSORTORDER TITLESORTORDER INVOLVEDPEOPLE),
260 "USERTEXT" => "USERTEXT",
262 "WEB" => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) ,
263 "WWWUSER" => "WWWUSER",
264 "LTEXT" => "TERMSOFUSE" ,
265 "PICTURE" => "PICTURE" ,
266 "UNSYNCEDLYRICS" => "UNSYNCEDLYRICS" ,
267 "COMMENT" => "COMMENT" ,
268 "BINARY" => %w(PLAYCOUNTER CDID) ,
270 # For the following Frames there are no parser stings defined .. the user has access to the raw data
271 # The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
273 "UNPARSED" => %w(UNIQUEFILEID OWNERSHIP SYNCEDTEMPO MPEGLOOKUP REVERB SYNCEDLYRICS CONTENTGROUP POPULARIMETER GENERALOBJECT VOLUMEADJ AUDIOCRYPTO CRYPTEDMETA BUFFERSIZE EVENTTIMING EQUALIZATION LINKED PRIVATE LINKEDINFO POSITIONSYNC GROUPINGREG CRYPTOREG COMMERCIAL SEEKFRAME AUDIOSEEKPOINT SIGNATURE EQUALIZATION2 VOLUMEADJ2 MUSICIANCREDITLIST INVOLVEDPEOPLE2 RECORDINGTIME ORIGRELEASETIME ENCODINGTIME RELEASETIME TAGGINGTIME)
279 # not sure if it's Z* or A*
280 # A* does not append a \0 when writing!
282 # STILL NEED TO CAREFULLY VERIFY THESE AGAINST THE STANDARDS AND GET TEST-CASES!
283 # seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
286 "TEXT" => [ %w(encoding text) , 'CZ*' ] ,
287 "USERTEXT" => [ %w(encoding description value) , 'CZ*Z*' ] ,
289 "PICTURE" => [ %w(encoding mimeType pictType description picture) , 'CZ*CZ*a*' ] ,
291 "WEB" => [ "url" , 'Z*' ] ,
292 "WWWUSER" => [ %w(encoding description url) , 'CZ*Z*' ] ,
294 "LTEXT" => [ %w(encoding language text) , 'CZ*Z*' ] ,
295 "UNSYNCEDLYRICS" => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
296 "COMMENT" => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
297 "BINARY" => [ "binary" , 'a*' ] ,
298 "UNPARSED" => [ "raw" , 'a*' ] # how would we do value checking for this?
301 # ----------------------------------------------------------------------------
303 # ----------------------------------------------------------------------------
304 Symbol2framename = ID3::SUPPORTED_SYMBOLS
305 Framename2symbol = Hash.new
306 Framename2symbol["1.0"] = ID3::SUPPORTED_SYMBOLS["1.0"].invert
307 Framename2symbol["1.1"] = ID3::SUPPORTED_SYMBOLS["1.1"].invert
308 Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
309 Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
310 Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
312 FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
314 FrameName2FrameType = FrameType2FrameName.invert
316 # ----------------------------------------------------------------------------
317 # the following piece of code is just for debugging, to sanity-check that all
318 # the FrameSymbols map back to a FrameType -- otherwise the library code will
319 # break if we encounter a Frame which can't be mapped to a FrameType..
320 # ----------------------------------------------------------------------------
322 # ensure we have a FrameType defined for each FrameName, otherwise
323 # code might break later..
326 # print "\nMISSING SYMBOLS:\n"
328 (ID3::Framename2symbol["2.2.0"].values +
329 ID3::Framename2symbol["2.3.0"].values +
330 ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
331 # print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
332 print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
336 # ----------------------------------------------------------------------------
338 # ----------------------------------------------------------------------------
339 # The ID3 module functions are to query or modify files directly.
340 # They give direct acess to files, and don't parse the tags, despite their headers
344 # ----------------------------------------------------------------------------
346 # returns string with version 1.0 or 1.1 if tag was found
347 # returns false otherwise
349 def ID3.hasID3v1tag?(filename)
352 # be careful with empty or corrupt files..
353 return false if File.size(filename) < ID3v1tagSize
355 f = File.open(filename, 'r')
356 f.seek(-ID3v1tagSize, IO::SEEK_END)
357 if (f.read(3) == "TAG")
358 f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
359 c = f.getc; # this is character 125 of the tag
370 # ----------------------------------------------------------------------------
372 # returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
373 # returns false otherwise
375 def ID3.hasID3v2tag?(filename)
378 f = File.open(filename, 'r')
379 if (f.read(3) == "ID3")
382 version = "2." + major.to_s + '.' + minor.to_s
383 hasID3v2tag = version
389 # ----------------------------------------------------------------------------
391 # returns string with all versions found, space separated
392 # returns false otherwise
394 def ID3.hasID3tag?(filename)
395 v1 = ID3.hasID3v1tag?(filename)
396 v2 = ID3.hasID3v2tag?(filename)
398 return false if !v1 && !v2
404 # ----------------------------------------------------------------------------
406 # returns nil if no v1 tag was found, or it couldn't be removed
407 # returns true if v1 tag found and it was removed..
410 # returns ID3.Tag1 object if a v1 tag was found and removed
412 def ID3.removeID3v1tag(filename)
413 stat = File.stat(filename)
414 if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
416 # CAREFUL: this does not check if there really is a valid tag:
418 newsize = stat.size - ID3v1tagSize
419 File.open(filename, "r+") { |f| f.truncate(newsize) }
426 # ----------------------------------------------------------------------------
429 # ==============================================================================
430 # Class AudioFile may call this ID3File
432 # reads and parses audio files for tags
433 # writes audio files and attaches dumped tags to it..
434 # revert feature would be nice to have..
436 # If we query and AudioFile object, we query what's currently associated with it
437 # e.g. we're not querying the file itself, but the perhaps modified tags
438 # To query the file itself, use the module functions
442 attr_reader :audioStartX , :audioEndX # begin and end indices of audio data in file
443 attr_reader :audioMD5sum # MD5sum of the audio portion of the file
445 attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
446 attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
448 attr_accessor :tagID3v1, :tagID3v2
449 attr_reader :hasID3tag # either false, or a string with all version numbers found
451 # ----------------------------------------------------------------------------
454 # AudioFile.new does NOT open the file, but scans it and parses the info
456 # e.g.: ID3::AudioFile.new('mp3/a.mp3')
458 def initialize(filename)
459 @filename = filename # similar to path method from class File, which is a mis-nomer!
461 @dirname = File.dirname( "#{@pwd}/#{@filename}" ) # just sugar
462 @basename = File.basename( "#{@pwd}/#{@filename}" ) # just sugar
468 audioEndX = File.size(filename)
470 if ID3.hasID3v1tag?(@filename)
472 @tagID3v1.read(@filename)
474 audioEndX -= ID3::ID3v1tagSize
476 if ID3.hasID3v2tag?(@filename)
478 @tagID3v2.read(@filename)
480 audioStartX = @tagID3v2.raw.size
483 # audioStartX audioEndX indices into the file need to be set
484 @audioStartX = audioStartX
485 @audioEndX = audioEndX
487 # user may compute the MD5sum of the audio content later..
488 # but we're only doing this if the user requests it..
493 # ----------------------------------------------------------------------------
495 # if the user tries to access @audioMD5sum, it will be computed for him,
496 # unless it was previously computed. We try to calculate that only once
497 # and on demand, because it's a bit expensive to compute..
502 File.open( File.join(@dirname,@basename) ) { |f|
504 @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
510 # ----------------------------------------------------------------------------
512 # write the filename and MD5sum of the audio portion into an ascii file
513 # in the same location as the audio file, but with suffix .md5
515 # computes the @audioMD5sum, if it wasn't previously computed..
519 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
521 base = @basename.sub( /(.)\.[^.]+$/ , '\1')
523 File.open( File.join(@dirname,base) ,"w") { |f|
524 f.printf("%s %s\n", File.join(@dirname,@basename), @audioMD5sum)
528 # ----------------------------------------------------------------------------
530 # compare the audioMD5sum against a previously stored md5sum file
531 # and returns boolean value of comparison
533 # If no md5sum file existed, we create one and return true.
535 # computes the @audioMD5sum, if it wasn't previously computed..
541 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
543 base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
544 base += '.md5' # add new suffix .md5
545 md5name = File.join(@dirname,base)
547 # if a MD5-file doesn't exist, we should create one and return TRUE ...
548 if File.exists?(md5name)
549 File.open( md5name ,"r") { |f|
550 oldname,oldMD5sum = f.readline.split # read old MD5-sum
553 oldMD5sum = self.writeMD5sum # create MD5-file and return true..
555 @audioMD5sum == oldMD5sum
558 # ----------------------------------------------------------------------------
561 a.push(@tagID3v1.version) if @tagID3v1
562 a.push(@tagID3v2.version) if @tagID3v2
563 return nil if a == []
566 alias versions version
567 # ----------------------------------------------------------------------------
571 end # of class AudioFile
574 # ==============================================================================
575 # Class RestrictedOrderedHash
577 class RestrictedOrderedHash < Hash
579 attr_accessor :count , :order, :locked
592 # alias old_store []=
599 # we're not allowed to add new keys!
600 raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
601 " valid key are: " + self.keys.join(",") +"\n"
619 # returns the human-readable ordered hash in correct order .. ;-)
624 self.order.each{ |key|
625 str += ", " if !first
628 str += (self[key]).inspect
634 # users can not delete entries from a locked hash..
636 alias old_delete delete
649 # ==============================================================================
652 # as per ID3-definition, the frames are in no fixed order! that's why Hash is OK
654 class GenericTag < Hash ###### should this be RestrictedOrderedHash as well?
655 attr_reader :version, :raw
657 # these definitions are to prevent users from inventing their own field names..
658 # but on the other hand, they should be able to create a new valid field, if
659 # it's not yet in the current tag, but it's valid for that ID3-version...
660 # ... so hiding this, is not enough!
665 # ----------------------------------------------------------------------
669 raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n"
671 if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
672 if !val.is_a?(ID3::Frame) and respond_to? :set_frame
679 raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
680 " valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
684 # ----------------------------------------------------------------------
685 # convert the 4 bytes found in the id3v2 header and return the size
687 def unmungeSize(bytes)
691 size += 128**i * (bytes[j] & 0x7f)
697 # ----------------------------------------------------------------------
698 # convert the size into 4 bytes to be written into an id3v2 header
700 def GenericTag.mungeSize(size)
701 bytes = Array.new(4,0)
704 bytes[j],size = size.divmod(128**i)
711 # ----------------------------------------------------------------------------
713 end # of class GenericTag
715 # ==============================================================================
716 # Class Tag1 ID3 Version 1.x Tag
718 # parses ID3v1 tags from a binary array
719 # dumps ID3v1 tags into a binary array
720 # allows to modify tag's contents
722 class Tag1 < GenericTag
724 # ----------------------------------------------------------------------
725 # read reads a version 1.x ID3tag
735 f = File.open(filename, 'r')
736 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
737 hastag = (f.read(3) == 'TAG')
739 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
740 @raw = f.read(ID3::ID3v1tagSize)
742 # self.parse!(raw) # we should use "parse!" instead of re-coding everything..
744 if (raw[ID3v1versionbyte] == 0)
750 @raw = @version = nil
754 # now parse all the fields
756 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
757 if val.class == Range
758 self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
759 elsif val.class == Fixnum
760 self[key] = @raw[val].to_s
762 # this can't happen the way we defined the hash..
763 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
768 # ----------------------------------------------------------------------
769 # write writes a version 1.x ID3tag
771 # not implemented yet..
773 # need to loacte old tag, and remove it, then append new tag..
775 # always upgrade version 1.0 to 1.1 when writing
778 # ----------------------------------------------------------------------
779 # this routine modifies self, e.g. the Tag1 object
781 # tag.parse!(raw) returns boolean value, showing if parsing was successful
785 return false if raw.size != ID3::ID3v1tagSize
787 if (raw[ID3v1versionbyte] == 0)
793 self.clear # remove all entries from Hash, we don't want left-overs..
795 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
796 if val.class == Range
797 self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
798 elsif val.class == Fixnum
799 self[key] = raw[val].to_s
801 # this can't happen the way we defined the hash..
802 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
808 # ----------------------------------------------------------------------
809 # dump version 1.1 ID3 Tag into a binary array
811 # although we provide this method, it's stongly discouraged to use it,
812 # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
813 # are often truncated and hence often useless..
817 raw = "\0" * ID3::ID3v1tagSize
820 self.each{ |key,value|
822 range = ID3::Symbol2framename['1.1'][key]
824 if range.class == Range
825 length = range.last - range.first + 1
826 paddedstring = value + zeroes
827 raw[range] = paddedstring[0..length-1]
828 elsif range.class == Fixnum
829 raw[range] = value.to_i
831 # this can't happen the way we defined the hash..
838 # ----------------------------------------------------------------------
841 # ==============================================================================
842 # Class Tag2 ID3 Version 2.x.y Tag
844 # parses ID3v2 tags from a binary array
845 # dumps ID3v2 tags into a binary array
846 # allows to modify tag's contents
848 # as per definition, the frames are in no fixed order
850 class Tag2 < GenericTag
852 attr_reader :rawflags, :flags
863 f = File.open(filename, 'r')
864 hastag = (f.read(3) == "ID3")
868 @version = "2." + major.to_s + '.' + minor.to_s
870 size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
874 # parse the raw flags:
875 if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
876 # in this case we need to skip parsing the frame... and skip to the next one...
877 wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
878 error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
879 raise ArgumentError, error
884 TAG_HEADER_FLAGS[@version].each{ |key,val|
885 # only define the flags which are set..
886 @flags[key] = true if (@rawflags & val == 1)
897 # now parse all the frames
899 i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
901 while (i < @raw.size) && (@raw[i] != 0)
902 len,frame = parse_frame_header(i) # this will create the correct frame
913 # ----------------------------------------------------------------------
916 # writes and replaces existing ID3-v2-tag if one is present
917 # Careful, this does NOT merge or append, it overwrites!
920 # check how long the old ID3-v2 tag is
924 # append old audio to new tag
927 # ----------------------------------------------------------------------
930 # each frame consists of a header of fixed length;
931 # depending on the ID3version, either 6 or 10 bytes.
932 # and of a data portion which is of variable length,
933 # and which contents might not be parsable by us
935 # INPUT: index to where in the @raw data the frame starts
936 # RETURNS: if successful parse:
937 # total size in bytes, ID3frame struct
942 # Struct of type ID3frame which contains:
943 # the name, size (in bytes), headerX,
944 # dataStartX, dataEndX, flags
945 # the data indices point into the @raw data, so we can cut out
946 # and parse the data at a later point in time.
948 # total frame size = dataEndX - headerX
949 # total header size= dataStartX - headerX
950 # total data size = dataEndX - dataStartX
953 def parse_frame_header(x)
954 framename = ""; flags = nil
957 if @version =~ /^2\.2\./
958 frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
959 header = @raw[x..x+frameHeaderSize-1]
961 framename = header[0..2]
962 size = (header[3]*256**2)+(header[4]*256)+header[5]
964 # printf "frame: %s , size: %d\n", framename , size
966 elsif @version =~ /^2\.[34]\./
967 # for version 2.3.0 and 2.4.0 the header is 10 bytes long
969 header = @raw[x..x+frameHeaderSize-1]
971 framename = header[0..3]
972 size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
974 # printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
977 # we can't parse higher versions
981 # if this is a valid frame of known type, we return it's total length and a struct
983 if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
984 frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
985 self[ Framename2symbol[@version][frame.name] ] = frame
986 return size+frameHeaderSize , frame
991 # ----------------------------------------------------------------------
992 # dump a ID3-v2 tag into a binary array
998 # dump all the frames
999 self.each { |framename,framedata|
1000 data << (framedata.dump || "")
1002 # add some padding perhaps
1005 # calculate the complete length of the data-section
1006 size = GenericTag.mungeSize(data.size)
1008 major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1010 # prepend a valid ID3-v2.x header to the data block
1011 header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1015 # ----------------------------------------------------------------------
1017 def set_frame(tag, contents)
1018 self[tag] = Frame.new(tag, version, contents)
1022 # ==============================================================================
1023 # Class Frame ID3 Version 2.x.y Frame
1025 # parses ID3v2 frames from a binary array
1026 # dumps ID3v2 frames into a binary array
1027 # allows to modify frame's contents if the frame was decoded..
1029 # NOTE: right now the class Frame is derived from Hash, which is wrong..
1030 # It should really be derived from something like RestrictedOrderedHash
1031 # ... a new class, which preserves the order of keys, and which does
1032 # strict checking that all keys are present and reference correct values!
1033 # e.g. frames["COMMENT"]
1034 # ==> {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
1036 # e.g. user should be able to create a new frame , like:
1037 # tag2.frames["COMMENT"] = "right side"
1039 # and the following checks should be done:
1041 # 1) if "COMMENT" is a correct key for tag2
1042 # 2) if the "right side" contains the correct keys
1043 # 3) if the "right side" contains the correct value for each key
1045 # In the simplest case, the "right side" might be just a string,
1046 # but for most FrameTypes, it's a complex datastructure.. and we need
1047 # to check it for correctness before doing the assignment..
1049 # NOTE2: the class Tag2 should have hash-like accessor functions to let the user
1050 # easily access frames and their contents..
1052 # e.g. tag2[framename] would really access tag2.frames[framename]
1054 # and if that works, we can make tag2.frames private and hidden!
1056 # This means, that when we generate the parse and dump routines dynamically,
1057 # we may want to create the corresponding accessor methods for Tag2 class
1058 # as well...? or are generic ones enough?
1061 class Frame < RestrictedOrderedHash
1062 attr_reader :rawflags, :flags
1063 attr_reader :name, :version
1064 attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
1065 ENCODINGS = ["latin1", "utf16", "utf16be", "utf8"]
1067 # ----------------------------------------------------------------------
1068 # return the complete raw frame
1071 return @rawheader + @rawdata
1073 # ----------------------------------------------------------------------
1074 alias old_init initialize
1084 def create(tag, version, value)
1086 @name = SUPPORTED_SYMBOLS[version][tag]
1088 self["encoding"] = 0
1089 self["text"] = value
1091 def recode(encoding)
1092 new_frame = self.clone
1093 new_frame["text"] = Iconv.conv(ENCODINGS[encoding], ENCODINGS[self["encoding"]], self["text"])
1094 new_frame["encoding"] = encoding
1097 def read(tag, name, headerStartX, dataStartX, dataEndX, flags)
1099 @headerStartX = headerStartX
1100 @dataStartX = dataStartX
1101 @dataEndX = dataEndX
1103 @rawdata = tag.raw[dataStartX..dataEndX]
1104 @rawheader = tag.raw[headerStartX..dataStartX-1]
1106 # initialize the super class..
1109 # parse the darn flags, if there are any..
1111 @version = tag.version # caching..
1114 # no flags, no extra attributes necessary
1119 @rawflags = flags.to_i # preserve the raw flags (for debugging only)
1121 if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1122 # in this case we need to skip parsing the frame... and skip to the next one...
1123 wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1124 error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1125 raise ArgumentError, error
1130 FRAME_HEADER_FLAGS[@version].each{ |key,val|
1131 # only define the flags which are set..
1132 @flags[key] = true if (flags.to_i & val == 1)
1136 raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1138 self.parse # now we're using the just defined parsing routine
1143 # here we GENERATE the code to parse, dump and verify methods
1145 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1147 # debugging print-out:
1149 if vars.class == Array
1150 vars2 = vars.join(",")
1155 values = self.rawdata.unpack(packing)
1157 self[key] = values.shift
1159 self.lock # lock the OrderedHash
1163 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1165 data = self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
1166 header = self.name.dup # we want the value! not the reference!!
1168 if self.version =~ /^2\.2\./
1169 byte2,rest = len.divmod(256**2)
1170 byte1,byte0 = rest.divmod(256)
1172 header << byte2 << byte1 << byte0
1174 elsif self.version =~ /^2\.[34]\./ # 10-byte header
1175 byte3,rest = len.divmod(256**3)
1176 byte2,rest = rest.divmod(256**2)
1177 byte1,byte0 = rest.divmod(256)
1179 flags1,flags0 = self.rawflags.divmod(256)
1181 header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1187 # ----------------------------------------------------------------------
1191 end # of class Frame
1193 # ==============================================================================