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) , 'Ca*' ] ,
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_writer :audioStartX , :audioEndX # begin and end indices of audio data in file
444 attr_reader :audioMD5sum # MD5sum of the audio portion of the file
446 attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
447 attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
449 attr_accessor :tagID3v1, :tagID3v2
450 attr_reader :hasID3tag # either false, or a string with all version numbers found
452 # ----------------------------------------------------------------------------
455 # AudioFile.new does NOT open the file, but scans it and parses the info
457 # e.g.: ID3::AudioFile.new('mp3/a.mp3')
459 def initialize(filename)
460 @filename = filename # similar to path method from class File, which is a mis-nomer!
462 @dirname = File.dirname( "#{@pwd}/#{@filename}" ) # just sugar
463 @basename = File.basename( "#{@pwd}/#{@filename}" ) # just sugar
469 audioEndX = File.size(filename)
471 if ID3.hasID3v1tag?(@filename)
473 @tagID3v1.read(@filename)
475 audioEndX -= ID3::ID3v1tagSize
477 if ID3.hasID3v2tag?(@filename)
479 @tagID3v2.read(@filename)
481 audioStartX = @tagID3v2.raw.size
484 # audioStartX audioEndX indices into the file need to be set
485 @audioStartX = audioStartX
486 @audioEndX = audioEndX
488 # user may compute the MD5sum of the audio content later..
489 # but we're only doing this if the user requests it..
494 # ----------------------------------------------------------------------------
496 # if the user tries to access @audioMD5sum, it will be computed for him,
497 # unless it was previously computed. We try to calculate that only once
498 # and on demand, because it's a bit expensive to compute..
503 File.open( File.join(@dirname,@basename) ) { |f|
505 @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
511 # ----------------------------------------------------------------------------
513 # write the filename and MD5sum of the audio portion into an ascii file
514 # in the same location as the audio file, but with suffix .md5
516 # computes the @audioMD5sum, if it wasn't previously computed..
520 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
522 base = @basename.sub( /(.)\.[^.]+$/ , '\1')
524 File.open( File.join(@dirname,base) ,"w") { |f|
525 f.printf("%s %s\n", File.join(@dirname,@basename), @audioMD5sum)
529 # ----------------------------------------------------------------------------
531 # compare the audioMD5sum against a previously stored md5sum file
532 # and returns boolean value of comparison
534 # If no md5sum file existed, we create one and return true.
536 # computes the @audioMD5sum, if it wasn't previously computed..
542 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
544 base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
545 base += '.md5' # add new suffix .md5
546 md5name = File.join(@dirname,base)
548 # if a MD5-file doesn't exist, we should create one and return TRUE ...
549 if File.exists?(md5name)
550 File.open( md5name ,"r") { |f|
551 oldname,oldMD5sum = f.readline.split # read old MD5-sum
554 oldMD5sum = self.writeMD5sum # create MD5-file and return true..
556 @audioMD5sum == oldMD5sum
559 # ----------------------------------------------------------------------------
562 a.push(@tagID3v1.version) if @tagID3v1
563 a.push(@tagID3v2.version) if @tagID3v2
564 return nil if a == []
567 alias versions version
568 # ----------------------------------------------------------------------------
572 end # of class AudioFile
575 # ==============================================================================
576 # Class RestrictedOrderedHash
578 class RestrictedOrderedHash < Hash
580 attr_accessor :count , :order, :locked
593 # alias old_store []=
600 # we're not allowed to add new keys!
601 raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
602 " valid key are: " + self.keys.join(",") +"\n"
620 # returns the human-readable ordered hash in correct order .. ;-)
625 self.order.each{ |key|
626 str += ", " if !first
629 str += (self[key]).inspect
635 # users can not delete entries from a locked hash..
637 alias old_delete delete
650 # ==============================================================================
653 # as per ID3-definition, the frames are in no fixed order! that's why Hash is OK
655 class GenericTag < Hash ###### should this be RestrictedOrderedHash as well?
656 attr_reader :version, :raw
658 # these definitions are to prevent users from inventing their own field names..
659 # but on the other hand, they should be able to create a new valid field, if
660 # it's not yet in the current tag, but it's valid for that ID3-version...
661 # ... so hiding this, is not enough!
666 # ----------------------------------------------------------------------
670 raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n"
672 if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
673 if !val.is_a?(ID3::Frame) and respond_to? :set_frame
680 raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
681 " valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
685 # ----------------------------------------------------------------------
686 # convert the 4 bytes found in the id3v2 header and return the size
688 def unmungeSize(bytes)
692 size += 128**i * (bytes[j] & 0x7f)
698 # ----------------------------------------------------------------------
699 # convert the size into 4 bytes to be written into an id3v2 header
701 def GenericTag.mungeSize(size)
702 bytes = Array.new(4,0)
705 bytes[j],size = size.divmod(128**i)
712 # ----------------------------------------------------------------------------
714 end # of class GenericTag
716 # ==============================================================================
717 # Class Tag1 ID3 Version 1.x Tag
719 # parses ID3v1 tags from a binary array
720 # dumps ID3v1 tags into a binary array
721 # allows to modify tag's contents
723 class Tag1 < GenericTag
725 # ----------------------------------------------------------------------
726 # read reads a version 1.x ID3tag
736 f = File.open(filename, 'r')
737 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
738 hastag = (f.read(3) == 'TAG')
740 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
741 @raw = f.read(ID3::ID3v1tagSize)
743 # self.parse!(raw) # we should use "parse!" instead of re-coding everything..
745 if (raw[ID3v1versionbyte] == 0)
751 @raw = @version = nil
755 # now parse all the fields
757 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
758 if val.class == Range
759 self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
760 elsif val.class == Fixnum
761 self[key] = @raw[val].to_s
763 # this can't happen the way we defined the hash..
764 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
769 # ----------------------------------------------------------------------
770 # write writes a version 1.x ID3tag
772 # not implemented yet..
774 # need to loacte old tag, and remove it, then append new tag..
776 # always upgrade version 1.0 to 1.1 when writing
779 # ----------------------------------------------------------------------
780 # this routine modifies self, e.g. the Tag1 object
782 # tag.parse!(raw) returns boolean value, showing if parsing was successful
786 return false if raw.size != ID3::ID3v1tagSize
788 if (raw[ID3v1versionbyte] == 0)
794 self.clear # remove all entries from Hash, we don't want left-overs..
796 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
797 if val.class == Range
798 self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
799 elsif val.class == Fixnum
800 self[key] = raw[val].to_s
802 # this can't happen the way we defined the hash..
803 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
809 # ----------------------------------------------------------------------
810 # dump version 1.1 ID3 Tag into a binary array
812 # although we provide this method, it's stongly discouraged to use it,
813 # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
814 # are often truncated and hence often useless..
818 raw = "\0" * ID3::ID3v1tagSize
821 self.each{ |key,value|
823 range = ID3::Symbol2framename['1.1'][key]
825 if range.class == Range
826 length = range.last - range.first + 1
827 paddedstring = value + zeroes
828 raw[range] = paddedstring[0..length-1]
829 elsif range.class == Fixnum
830 raw[range] = value.to_i
832 # this can't happen the way we defined the hash..
839 # ----------------------------------------------------------------------
842 # ==============================================================================
843 # Class Tag2 ID3 Version 2.x.y Tag
845 # parses ID3v2 tags from a binary array
846 # dumps ID3v2 tags into a binary array
847 # allows to modify tag's contents
849 # as per definition, the frames are in no fixed order
851 class Tag2 < GenericTag
853 attr_reader :rawflags, :flags
854 attr_writer :version, :raw
864 f = File.open(filename, 'r')
865 hastag = (f.read(3) == "ID3")
868 size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
880 hastag = (@raw[0...3] == "ID3")
884 @version = "2." + major.to_s + '.' + minor.to_s
886 size = ID3::ID3v2headerSize + unmungeSize(@raw[6...10])
888 # parse the raw flags:
889 if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
890 # in this case we need to skip parsing the frame... and skip to the next one...
891 wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
892 error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
893 raise ArgumentError, error
898 TAG_HEADER_FLAGS[@version].each{ |key,val|
899 # only define the flags which are set..
900 @flags[key] = true if (@rawflags & val == 1)
911 # now parse all the frames
913 i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
915 while (i < @raw.size) && (@raw[i] != 0)
916 len,frame = parse_frame_header(i) # this will create the correct frame
927 # ----------------------------------------------------------------------
930 # writes and replaces existing ID3-v2-tag if one is present
931 # Careful, this does NOT merge or append, it overwrites!
934 # check how long the old ID3-v2 tag is
938 # append old audio to new tag
941 # ----------------------------------------------------------------------
944 # each frame consists of a header of fixed length;
945 # depending on the ID3version, either 6 or 10 bytes.
946 # and of a data portion which is of variable length,
947 # and which contents might not be parsable by us
949 # INPUT: index to where in the @raw data the frame starts
950 # RETURNS: if successful parse:
951 # total size in bytes, ID3frame struct
956 # Struct of type ID3frame which contains:
957 # the name, size (in bytes), headerX,
958 # dataStartX, dataEndX, flags
959 # the data indices point into the @raw data, so we can cut out
960 # and parse the data at a later point in time.
962 # total frame size = dataEndX - headerX
963 # total header size= dataStartX - headerX
964 # total data size = dataEndX - dataStartX
967 def parse_frame_header(x)
968 framename = ""; flags = nil
971 if @version =~ /^2\.2\./
972 frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
973 header = @raw[x..x+frameHeaderSize-1]
975 framename = header[0..2]
976 size = (header[3]*256**2)+(header[4]*256)+header[5]
978 # printf "frame: %s , size: %d\n", framename , size
980 elsif @version =~ /^2\.[34]\./
981 # for version 2.3.0 and 2.4.0 the header is 10 bytes long
983 header = @raw[x..x+frameHeaderSize-1]
985 framename = header[0..3]
986 size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
988 # printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
991 # we can't parse higher versions
995 # if this is a valid frame of known type, we return it's total length and a struct
997 if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
998 frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
999 self[ Framename2symbol[@version][frame.name] ] = frame
1000 return size+frameHeaderSize , frame
1005 # ----------------------------------------------------------------------
1006 # dump a ID3-v2 tag into a binary array
1012 # dump all the frames
1013 self.each { |framename,framedata|
1014 data << (framedata.dump || "")
1016 # add some padding perhaps
1019 # calculate the complete length of the data-section
1020 size = GenericTag.mungeSize(data.size)
1022 major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1024 # prepend a valid ID3-v2.x header to the data block
1025 header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1029 # ----------------------------------------------------------------------
1031 def set_frame(tag, contents)
1032 self[tag] = Frame.new(tag, version, contents)
1036 # ==============================================================================
1037 # Class Frame ID3 Version 2.x.y Frame
1039 # parses ID3v2 frames from a binary array
1040 # dumps ID3v2 frames into a binary array
1041 # allows to modify frame's contents if the frame was decoded..
1043 # NOTE: right now the class Frame is derived from Hash, which is wrong..
1044 # It should really be derived from something like RestrictedOrderedHash
1045 # ... a new class, which preserves the order of keys, and which does
1046 # strict checking that all keys are present and reference correct values!
1047 # e.g. frames["COMMENT"]
1048 # ==> {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
1050 # e.g. user should be able to create a new frame , like:
1051 # tag2.frames["COMMENT"] = "right side"
1053 # and the following checks should be done:
1055 # 1) if "COMMENT" is a correct key for tag2
1056 # 2) if the "right side" contains the correct keys
1057 # 3) if the "right side" contains the correct value for each key
1059 # In the simplest case, the "right side" might be just a string,
1060 # but for most FrameTypes, it's a complex datastructure.. and we need
1061 # to check it for correctness before doing the assignment..
1063 # NOTE2: the class Tag2 should have hash-like accessor functions to let the user
1064 # easily access frames and their contents..
1066 # e.g. tag2[framename] would really access tag2.frames[framename]
1068 # and if that works, we can make tag2.frames private and hidden!
1070 # This means, that when we generate the parse and dump routines dynamically,
1071 # we may want to create the corresponding accessor methods for Tag2 class
1072 # as well...? or are generic ones enough?
1075 class Frame < RestrictedOrderedHash
1076 attr_reader :rawflags, :flags
1077 attr_reader :name, :version
1078 attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
1079 ENCODINGS = ["latin1", "utf16", "utf16be", "utf8"]
1081 # ----------------------------------------------------------------------
1082 # return the complete raw frame
1085 return @rawheader + @rawdata
1087 # ----------------------------------------------------------------------
1088 alias old_init initialize
1098 def create(tag, version, value)
1100 @name = SUPPORTED_SYMBOLS[version][tag]
1102 self["encoding"] = 0
1103 self["text"] = value
1105 def recode(encoding)
1106 self["text"] = Iconv.conv(ENCODINGS[encoding], ENCODINGS[self["encoding"]], self["text"])
1107 self["encoding"] = encoding
1109 def read(tag, name, headerStartX, dataStartX, dataEndX, flags)
1111 @headerStartX = headerStartX
1112 @dataStartX = dataStartX
1113 @dataEndX = dataEndX
1115 @rawdata = tag.raw[dataStartX..dataEndX]
1116 @rawheader = tag.raw[headerStartX..dataStartX-1]
1118 # initialize the super class..
1121 # parse the darn flags, if there are any..
1123 @version = tag.version # caching..
1126 # no flags, no extra attributes necessary
1131 @rawflags = flags.to_i # preserve the raw flags (for debugging only)
1133 if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1134 # in this case we need to skip parsing the frame... and skip to the next one...
1135 wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1136 error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1137 raise ArgumentError, error
1142 FRAME_HEADER_FLAGS[@version].each{ |key,val|
1143 # only define the flags which are set..
1144 @flags[key] = true if (flags.to_i & val == 1)
1148 raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1150 self.parse # now we're using the just defined parsing routine
1155 # here we GENERATE the code to parse, dump and verify methods
1157 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1159 # debugging print-out:
1161 if vars.class == Array
1162 vars2 = vars.join(",")
1167 values = self.rawdata.unpack(packing)
1169 self[key] = values.shift
1171 self.lock # lock the OrderedHash
1175 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1177 data = self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
1178 header = self.name.dup # we want the value! not the reference!!
1180 if self.version =~ /^2\.2\./
1181 byte2,rest = len.divmod(256**2)
1182 byte1,byte0 = rest.divmod(256)
1184 header << byte2 << byte1 << byte0
1186 elsif self.version =~ /^2\.[34]\./ # 10-byte header
1187 byte3,rest = len.divmod(256**3)
1188 byte2,rest = rest.divmod(256**2)
1189 byte1,byte0 = rest.divmod(256)
1191 flags1,flags0 = self.rawflags.divmod(256)
1193 header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1199 # ----------------------------------------------------------------------
1203 end # of class Frame
1205 # ==============================================================================