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>
6 # Copyright (C) 2007-2008 by RafaĆ Rzepecki <divided.mind@gmail.com>
9 # updated: Time-stamp: <Mon 27-Dec-2004 22:23:49 Tilo Sloboda>
11 # Docs: http://www.id3.org/id3v2-00.txt
12 # http://www.id3.org/id3v2.3.0.txt
13 # http://www.id3.org/id3v2.4.0-changes.txt
14 # http://www.id3.org/id3v2.4.0-structure.txt
15 # http://www.id3.org/id3v2.4.0-frames.txt
17 # different versions of ID3 tags, support different fields.
18 # See: http://www.unixgods.org/~tilo/ID3v2_frames_comparison.txt
19 # See: http://www.unixgods.org/~tilo/ID3/docs/ID3_comparison.html
22 # Freely available under the terms of the OpenSource "Artistic License"
23 # in combination with the Addendum A (below)
25 # In case you did not get a copy of the license along with the software,
26 # it is also available at: http://www.unixgods.org/~tilo/artistic-license.html
29 # THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
30 # SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
31 # REPAIR OR CORRECTION.
33 # IN NO EVENT WILL THE COPYRIGHT HOLDERS BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
34 # SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY
35 # TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
36 # INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM
37 # TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
38 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
41 # The author of this ID3-library for Ruby is not responsible in any way for
42 # the definition of the ID3-standards..
44 # You're lucky though that you can use this little library, rather than having
45 # to parse ID3v2 tags yourself! Trust me! At the first glance it doesn't seem
46 # to be so complicated, but the ID3v2 definitions are so convoluted and
47 # unnecessarily complicated, with so many useless frame-types, it's a pain to
48 # read the documents describing the ID3 V2.x standards.. and even worse
51 # I don't know what these people were thinking... can we make it any more
52 # complicated than that?? ID3 version 2.4.0 tops everything! If this flag
53 # is set and it's a full moon, and an even weekday number, then do this..
54 # Outch!!! I assume that's why I don't find any 2.4.0 tags in any of my
55 # MP3-files... seems like noone is writing 2.4.0 tags... iTunes writes 2.3.0
57 # If you have some files with valid 2.4.0 tags, please send them my way!
60 #-------------------------------------------------------------------------------
64 # hasID3v1tag?(filename)
65 # hasID3v2tag?(filename)
66 # removeID3v1tag(filename)
74 ################################################################################
76 # ==============================================================================
77 # Lading other stuff..
78 # ==============================================================================
82 require 'hexdump' # load hexdump method to extend class String
83 require 'invert_hash' # new invert method for old Hash
86 class Hash # overwrite Hash.invert method
87 alias old_invert invert
97 # ----------------------------------------------------------------------------
99 # ----------------------------------------------------------------------------
100 @@RCSid = '$Id: id3.rb,v 1.2 2004/11/29 05:18:44 tilo Exp tilo $'
102 ID3v1tagSize = 128 # ID3v1 and ID3v1.1 have fixed size tags
103 ID3v1versionbyte = 125
107 SUPPORTED_SYMBOLS = {
108 "1.0" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
109 "YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
112 "1.1" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
113 "YEAR"=>93..96 , "COMMENT"=>97..124,
114 "TRACKNUM"=>126, "GENREID"=>127,
118 "2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
119 "ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
120 "COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
121 "ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC",
122 "DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
123 "ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT",
124 "COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN",
125 "ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
126 "PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
127 "ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
129 "WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
130 "WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
131 "WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
132 "INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
133 "CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
134 "SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
135 "EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
136 "GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
137 "BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
141 "2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
142 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
143 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
144 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
145 "DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
146 "ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ",
147 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
148 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
149 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
150 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
151 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
152 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
153 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
154 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
155 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
156 "INVOLVEDPEOPLE"=>"IPLS",
157 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
158 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
159 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
160 "VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA",
161 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
162 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
163 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
164 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
168 "2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
169 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
170 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
171 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
172 "RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
173 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
174 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
175 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
176 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
177 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
178 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
179 "SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
180 "ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
181 "ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
182 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
183 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
184 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
185 "MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
186 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
187 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
188 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
189 "VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
190 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
191 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
192 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
193 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
195 "OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
196 "AUDIOSEEKPOINT"=>"ASPI"
200 # ----------------------------------------------------------------------------
201 # Flags in the ID3-Tag Header:
203 TAG_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
204 # those flags are supposed to be zero!
205 "2.2.0" => 0x3F, # 0xC0 ,
206 "2.3.0" => 0x1F, # 0xE0 ,
207 "2.4.0" => 0x0F # 0xF0
211 "2.2.0" => { "Unsynchronisation" => 0x80 ,
212 "Compression" => 0x40 ,
214 "2.3.0" => { "Unsynchronisation" => 0x80 ,
215 "ExtendedHeader" => 0x40 ,
216 "Experimental" => 0x20 ,
218 "2.4.0" => { "Unsynchronisation" => 0x80 ,
219 "ExtendedHeader" => 0x40 ,
220 "Experimental" => 0x20 ,
225 # ----------------------------------------------------------------------------
226 # Flags in the ID3-Frame Header:
228 FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
229 # those flags are supposed to be zero!
230 "2.3.0" => 0x1F1F, # 0xD0D0 ,
231 "2.4.0" => 0x8FB0 # 0x704F ,
234 FRAME_HEADER_FLAGS = {
235 "2.3.0" => { "TagAlterPreservation" => 0x8000 ,
236 "FileAlterPreservation" => 0x4000 ,
237 "ReadOnly" => 0x2000 ,
239 "Compression" => 0x0080 ,
240 "Encryption" => 0x0040 ,
241 "GroupIdentity" => 0x0020 ,
243 "2.4.0" => { "TagAlterPreservation" => 0x4000 ,
244 "FileAlterPreservation" => 0x2000 ,
245 "ReadOnly" => 0x1000 ,
247 "GroupIdentity" => 0x0040 ,
248 "Compression" => 0x0008 ,
249 "Encryption" => 0x0004 ,
250 "Unsynchronisation" => 0x0002 ,
251 "DataLengthIndicator" => 0x0001 ,
255 # the FrameTypes are not visible to the user - they are just a mechanism
256 # to define only one parser for multiple FraneNames..
259 FRAMETYPE2FRAMENAME = {
260 "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),
261 "USERTEXT" => "USERTEXT",
263 "WEB" => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) ,
264 "WWWUSER" => "WWWUSER",
265 "LTEXT" => "TERMSOFUSE" ,
266 "PICTURE" => "PICTURE" ,
267 "UNSYNCEDLYRICS" => "UNSYNCEDLYRICS" ,
268 "COMMENT" => "COMMENT" ,
269 "BINARY" => %w(PLAYCOUNTER CDID) ,
271 # For the following Frames there are no parser stings defined .. the user has access to the raw data
272 # The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
274 "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)
280 # not sure if it's Z* or A*
281 # A* does not append a \0 when writing!
283 # STILL NEED TO CAREFULLY VERIFY THESE AGAINST THE STANDARDS AND GET TEST-CASES!
284 # seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
287 "TEXT" => [ %w(encoding text) , 'Ca*' ] ,
288 "USERTEXT" => [ %w(encoding description value) , 'Ca*a*' ] ,
290 "PICTURE" => [ %w(encoding mimeType pictType description picture) , 'CZ*Ca*a*' ] ,
292 "WEB" => [ "url" , 'a*' ] ,
293 "WWWUSER" => [ %w(encoding description url) , 'CZ*Z*' ] ,
295 "LTEXT" => [ %w(encoding language text) , 'CZ*Z*' ] ,
296 "UNSYNCEDLYRICS" => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
297 "COMMENT" => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
298 "BINARY" => [ "binary" , 'a*' ] ,
299 "UNPARSED" => [ "raw" , 'a*' ] # how would we do value checking for this?
302 # ----------------------------------------------------------------------------
304 # ----------------------------------------------------------------------------
305 Symbol2framename = ID3::SUPPORTED_SYMBOLS
306 Framename2symbol = Hash.new
307 Framename2symbol["1.0"] = ID3::SUPPORTED_SYMBOLS["1.0"].invert
308 Framename2symbol["1.1"] = ID3::SUPPORTED_SYMBOLS["1.1"].invert
309 Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
310 Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
311 Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
313 FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
315 FrameName2FrameType = FrameType2FrameName.invert
317 # ----------------------------------------------------------------------------
318 # the following piece of code is just for debugging, to sanity-check that all
319 # the FrameSymbols map back to a FrameType -- otherwise the library code will
320 # break if we encounter a Frame which can't be mapped to a FrameType..
321 # ----------------------------------------------------------------------------
323 # ensure we have a FrameType defined for each FrameName, otherwise
324 # code might break later..
327 # print "\nMISSING SYMBOLS:\n"
329 (ID3::Framename2symbol["2.2.0"].values +
330 ID3::Framename2symbol["2.3.0"].values +
331 ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
332 # print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
333 print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
337 # ----------------------------------------------------------------------------
339 # ----------------------------------------------------------------------------
340 # The ID3 module functions are to query or modify files directly.
341 # They give direct acess to files, and don't parse the tags, despite their headers
345 # ----------------------------------------------------------------------------
347 # returns string with version 1.0 or 1.1 if tag was found
348 # returns false otherwise
350 def ID3.hasID3v1tag?(filename)
353 # be careful with empty or corrupt files..
354 return false if File.size(filename) < ID3v1tagSize
356 f = File.open(filename, 'r')
357 f.seek(-ID3v1tagSize, IO::SEEK_END)
358 if (f.read(3) == "TAG")
359 f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
360 c = f.getc; # this is character 125 of the tag
371 # ----------------------------------------------------------------------------
373 # returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
374 # returns false otherwise
376 def ID3.hasID3v2tag?(filename)
379 f = File.open(filename, 'r')
380 if (f.read(3) == "ID3")
383 version = "2." + major.to_s + '.' + minor.to_s
384 hasID3v2tag = version
390 # ----------------------------------------------------------------------------
392 # returns string with all versions found, space separated
393 # returns false otherwise
395 def ID3.hasID3tag?(filename)
396 v1 = ID3.hasID3v1tag?(filename)
397 v2 = ID3.hasID3v2tag?(filename)
399 return false if !v1 && !v2
405 # ----------------------------------------------------------------------------
407 # returns nil if no v1 tag was found, or it couldn't be removed
408 # returns true if v1 tag found and it was removed..
411 # returns ID3.Tag1 object if a v1 tag was found and removed
413 def ID3.removeID3v1tag(filename)
414 stat = File.stat(filename)
415 if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
417 # CAREFUL: this does not check if there really is a valid tag:
419 newsize = stat.size - ID3v1tagSize
420 File.open(filename, "r+") { |f| f.truncate(newsize) }
427 # ----------------------------------------------------------------------------
430 # ==============================================================================
431 # Class AudioFile may call this ID3File
433 # reads and parses audio files for tags
434 # writes audio files and attaches dumped tags to it..
435 # revert feature would be nice to have..
437 # If we query and AudioFile object, we query what's currently associated with it
438 # e.g. we're not querying the file itself, but the perhaps modified tags
439 # To query the file itself, use the module functions
443 attr_reader :audioStartX , :audioEndX # begin and end indices of audio data in file
444 attr_writer :audioStartX , :audioEndX # begin and end indices of audio data in file
445 attr_reader :audioMD5sum # MD5sum of the audio portion of the file
447 attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
448 attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
450 attr_accessor :tagID3v1, :tagID3v2
451 attr_reader :hasID3tag # either false, or a string with all version numbers found
453 # ----------------------------------------------------------------------------
456 # AudioFile.new does NOT open the file, but scans it and parses the info
458 # e.g.: ID3::AudioFile.new('mp3/a.mp3')
460 def initialize(filename)
461 @filename = filename # similar to path method from class File, which is a mis-nomer!
463 @dirname = File.dirname( "#{@pwd}/#{@filename}" ) # just sugar
464 @basename = File.basename( "#{@pwd}/#{@filename}" ) # just sugar
470 audioEndX = File.size(filename)
472 if ID3.hasID3v1tag?(@filename)
474 @tagID3v1.read(@filename)
476 audioEndX -= ID3::ID3v1tagSize
478 if ID3.hasID3v2tag?(@filename)
480 @tagID3v2.read(@filename)
482 audioStartX = @tagID3v2.raw.size
485 # audioStartX audioEndX indices into the file need to be set
486 @audioStartX = audioStartX
487 @audioEndX = audioEndX
489 # user may compute the MD5sum of the audio content later..
490 # but we're only doing this if the user requests it..
495 # ----------------------------------------------------------------------------
497 # if the user tries to access @audioMD5sum, it will be computed for him,
498 # unless it was previously computed. We try to calculate that only once
499 # and on demand, because it's a bit expensive to compute..
504 File.open( File.join(@dirname,@basename) ) { |f|
506 @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
512 # ----------------------------------------------------------------------------
514 # write the filename and MD5sum of the audio portion into an ascii file
515 # in the same location as the audio file, but with suffix .md5
517 # computes the @audioMD5sum, if it wasn't previously computed..
521 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
523 base = @basename.sub( /(.)\.[^.]+$/ , '\1')
525 File.open( File.join(@dirname,base) ,"w") { |f|
526 f.printf("%s %s\n", File.join(@dirname,@basename), @audioMD5sum)
530 # ----------------------------------------------------------------------------
532 # compare the audioMD5sum against a previously stored md5sum file
533 # and returns boolean value of comparison
535 # If no md5sum file existed, we create one and return true.
537 # computes the @audioMD5sum, if it wasn't previously computed..
543 self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
545 base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
546 base += '.md5' # add new suffix .md5
547 md5name = File.join(@dirname,base)
549 # if a MD5-file doesn't exist, we should create one and return TRUE ...
550 if File.exists?(md5name)
551 File.open( md5name ,"r") { |f|
552 oldname,oldMD5sum = f.readline.split # read old MD5-sum
555 oldMD5sum = self.writeMD5sum # create MD5-file and return true..
557 @audioMD5sum == oldMD5sum
560 # ----------------------------------------------------------------------------
563 a.push(@tagID3v1.version) if @tagID3v1
564 a.push(@tagID3v2.version) if @tagID3v2
565 return nil if a == []
568 alias versions version
569 # ----------------------------------------------------------------------------
573 end # of class AudioFile
576 # ==============================================================================
577 # Class RestrictedOrderedHash
579 class RestrictedOrderedHash < Hash
581 attr_accessor :count , :order, :locked
594 # alias old_store []=
601 # we're not allowed to add new keys!
602 raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
603 " valid key are: " + self.keys.join(",") +"\n"
621 # returns the human-readable ordered hash in correct order .. ;-)
626 self.order.each{ |key|
627 str += ", " if !first
630 str += (self[key]).inspect
636 # users can not delete entries from a locked hash..
638 alias old_delete delete
651 # ==============================================================================
654 # as per ID3-definition, the frames are in no fixed order! that's why Hash is OK
656 class GenericTag < Hash ###### should this be RestrictedOrderedHash as well?
657 attr_reader :version, :raw
659 # these definitions are to prevent users from inventing their own field names..
660 # but on the other hand, they should be able to create a new valid field, if
661 # it's not yet in the current tag, but it's valid for that ID3-version...
662 # ... so hiding this, is not enough!
667 # ----------------------------------------------------------------------
671 raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n"
673 if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
674 if !val.is_a?(ID3::Frame) and respond_to? :set_frame
681 raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
682 " valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
686 # ----------------------------------------------------------------------
687 # convert the 4 bytes found in the id3v2 header and return the size
689 def unmungeSize(bytes)
693 size += 128**i * (bytes[j] & 0x7f)
699 # ----------------------------------------------------------------------
700 # convert the size into 4 bytes to be written into an id3v2 header
702 def GenericTag.mungeSize(size)
703 bytes = Array.new(4,0)
706 bytes[j],size = size.divmod(128**i)
713 # ----------------------------------------------------------------------------
715 end # of class GenericTag
717 # ==============================================================================
718 # Class Tag1 ID3 Version 1.x Tag
720 # parses ID3v1 tags from a binary array
721 # dumps ID3v1 tags into a binary array
722 # allows to modify tag's contents
724 class Tag1 < GenericTag
726 # ----------------------------------------------------------------------
727 # read reads a version 1.x ID3tag
737 f = File.open(filename, 'r')
738 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
739 hastag = (f.read(3) == 'TAG')
741 f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
742 @raw = f.read(ID3::ID3v1tagSize)
744 # self.parse!(raw) # we should use "parse!" instead of re-coding everything..
746 if (raw[ID3v1versionbyte] == 0)
752 @raw = @version = nil
756 # now parse all the fields
758 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
759 if val.class == Range
760 self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
761 elsif val.class == Fixnum
762 self[key] = @raw[val].to_s
764 # this can't happen the way we defined the hash..
765 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
770 # ----------------------------------------------------------------------
771 # write writes a version 1.x ID3tag
773 # not implemented yet..
775 # need to loacte old tag, and remove it, then append new tag..
777 # always upgrade version 1.0 to 1.1 when writing
780 # ----------------------------------------------------------------------
781 # this routine modifies self, e.g. the Tag1 object
783 # tag.parse!(raw) returns boolean value, showing if parsing was successful
787 return false if raw.size != ID3::ID3v1tagSize
789 if (raw[ID3v1versionbyte] == 0)
795 self.clear # remove all entries from Hash, we don't want left-overs..
797 ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
798 if val.class == Range
799 self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
800 elsif val.class == Fixnum
801 self[key] = raw[val].to_s
803 # this can't happen the way we defined the hash..
804 # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
810 # ----------------------------------------------------------------------
811 # dump version 1.1 ID3 Tag into a binary array
813 # although we provide this method, it's stongly discouraged to use it,
814 # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
815 # are often truncated and hence often useless..
819 raw = "\0" * ID3::ID3v1tagSize
822 self.each{ |key,value|
824 range = ID3::Symbol2framename['1.1'][key]
826 if range.class == Range
827 length = range.last - range.first + 1
828 paddedstring = value + zeroes
829 raw[range] = paddedstring[0..length-1]
830 elsif range.class == Fixnum
831 raw[range] = value.to_i
833 # this can't happen the way we defined the hash..
840 # ----------------------------------------------------------------------
843 # ==============================================================================
844 # Class Tag2 ID3 Version 2.x.y Tag
846 # parses ID3v2 tags from a binary array
847 # dumps ID3v2 tags into a binary array
848 # allows to modify tag's contents
850 # as per definition, the frames are in no fixed order
852 class Tag2 < GenericTag
854 attr_reader :rawflags, :flags
855 attr_writer :version, :raw
865 f = File.open(filename, 'r')
866 hastag = (f.read(3) == "ID3")
869 size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
881 hastag = (@raw[0...3] == "ID3")
885 @version = "2." + major.to_s + '.' + minor.to_s
887 size = ID3::ID3v2headerSize + unmungeSize(@raw[6...10])
889 # parse the raw flags:
890 if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
891 # in this case we need to skip parsing the frame... and skip to the next one...
892 wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
893 error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
894 raise ArgumentError, error
899 TAG_HEADER_FLAGS[@version].each{ |key,val|
900 # only define the flags which are set..
901 @flags[key] = true if (@rawflags & val == 1)
912 # now parse all the frames
914 i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
916 while (i < @raw.size) && (@raw[i] != 0)
917 len,frame = parse_frame_header(i) # this will create the correct frame
928 # ----------------------------------------------------------------------
931 # writes and replaces existing ID3-v2-tag if one is present
932 # Careful, this does NOT merge or append, it overwrites!
935 # check how long the old ID3-v2 tag is
939 # append old audio to new tag
942 # ----------------------------------------------------------------------
945 # each frame consists of a header of fixed length;
946 # depending on the ID3version, either 6 or 10 bytes.
947 # and of a data portion which is of variable length,
948 # and which contents might not be parsable by us
950 # INPUT: index to where in the @raw data the frame starts
951 # RETURNS: if successful parse:
952 # total size in bytes, ID3frame struct
957 # Struct of type ID3frame which contains:
958 # the name, size (in bytes), headerX,
959 # dataStartX, dataEndX, flags
960 # the data indices point into the @raw data, so we can cut out
961 # and parse the data at a later point in time.
963 # total frame size = dataEndX - headerX
964 # total header size= dataStartX - headerX
965 # total data size = dataEndX - dataStartX
968 def parse_frame_header(x)
969 framename = ""; flags = nil
972 if @version =~ /^2\.2\./
973 frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
974 header = @raw[x..x+frameHeaderSize-1]
976 framename = header[0..2]
977 size = (header[3]*256**2)+(header[4]*256)+header[5]
979 # printf "frame: %s , size: %d\n", framename , size
981 elsif @version =~ /^2\.[34]\./
982 # for version 2.3.0 and 2.4.0 the header is 10 bytes long
984 header = @raw[x..x+frameHeaderSize-1]
986 framename = header[0..3]
987 size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
989 # printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
992 # we can't parse higher versions
996 # if this is a valid frame of known type, we return it's total length and a struct
998 if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
999 frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
1000 self[ Framename2symbol[@version][frame.name] ] = frame
1001 return size+frameHeaderSize , frame
1006 # ----------------------------------------------------------------------
1007 # dump a ID3-v2 tag into a binary array
1013 # dump all the frames
1014 self.each { |framename,framedata|
1015 data << (framedata.dump || "")
1017 # add some padding perhaps
1020 # calculate the complete length of the data-section
1021 size = GenericTag.mungeSize(data.size)
1023 major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1025 # prepend a valid ID3-v2.x header to the data block
1026 header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1030 # ----------------------------------------------------------------------
1032 def set_frame(tag, contents)
1033 self[tag] = Frame.new(tag, version, contents)
1037 # ==============================================================================
1038 # Class Frame ID3 Version 2.x.y Frame
1040 # parses ID3v2 frames from a binary array
1041 # dumps ID3v2 frames into a binary array
1042 # allows to modify frame's contents if the frame was decoded..
1044 # NOTE: right now the class Frame is derived from Hash, which is wrong..
1045 # It should really be derived from something like RestrictedOrderedHash
1046 # ... a new class, which preserves the order of keys, and which does
1047 # strict checking that all keys are present and reference correct values!
1048 # e.g. frames["COMMENT"]
1049 # ==> {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
1051 # e.g. user should be able to create a new frame , like:
1052 # tag2.frames["COMMENT"] = "right side"
1054 # and the following checks should be done:
1056 # 1) if "COMMENT" is a correct key for tag2
1057 # 2) if the "right side" contains the correct keys
1058 # 3) if the "right side" contains the correct value for each key
1060 # In the simplest case, the "right side" might be just a string,
1061 # but for most FrameTypes, it's a complex datastructure.. and we need
1062 # to check it for correctness before doing the assignment..
1064 # NOTE2: the class Tag2 should have hash-like accessor functions to let the user
1065 # easily access frames and their contents..
1067 # e.g. tag2[framename] would really access tag2.frames[framename]
1069 # and if that works, we can make tag2.frames private and hidden!
1071 # This means, that when we generate the parse and dump routines dynamically,
1072 # we may want to create the corresponding accessor methods for Tag2 class
1073 # as well...? or are generic ones enough?
1076 class Frame < RestrictedOrderedHash
1077 attr_reader :rawflags, :flags
1078 attr_reader :name, :version
1079 attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
1080 ENCODINGS = ["latin1", "utf16", "utf16be", "utf8"]
1082 # ----------------------------------------------------------------------
1083 # return the complete raw frame
1086 return @rawheader + @rawdata
1088 # ----------------------------------------------------------------------
1089 alias old_init initialize
1095 elsif a.length == 3 or a.length == 4
1099 def create(tag, version, value, encoding = 0)
1101 @name = SUPPORTED_SYMBOLS[version][tag]
1106 def recode(encoding)
1107 self["text"] = Iconv.conv(ENCODINGS[encoding], ENCODINGS[self["encoding"]], self["text"])
1108 self["encoding"] = encoding
1111 def read(tag, name, headerStartX, dataStartX, dataEndX, flags)
1113 @headerStartX = headerStartX
1114 @dataStartX = dataStartX
1115 @dataEndX = dataEndX
1117 @rawdata = tag.raw[dataStartX..dataEndX]
1118 @rawheader = tag.raw[headerStartX..dataStartX-1]
1120 # initialize the super class..
1123 # parse the darn flags, if there are any..
1125 @version = tag.version # caching..
1128 # no flags, no extra attributes necessary
1133 @rawflags = flags.to_i # preserve the raw flags (for debugging only)
1135 if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1136 # in this case we need to skip parsing the frame... and skip to the next one...
1137 wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1138 error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1139 raise ArgumentError, error
1144 FRAME_HEADER_FLAGS[@version].each{ |key,val|
1145 # only define the flags which are set..
1146 @flags[key] = true if (flags.to_i & val == 1)
1150 raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1152 self.parse # now we're using the just defined parsing routine
1157 # here we GENERATE the code to parse, dump and verify methods
1159 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1161 # debugging print-out:
1163 if vars.class == Array
1164 vars2 = vars.join(",")
1169 values = self.rawdata.unpack(packing)
1171 self[key] = values.shift
1173 self.lock # lock the OrderedHash
1177 vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1178 self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
1182 header = self.name.dup # we want the value! not the reference!!
1184 if self.version =~ /^2\.2\./
1185 byte2,rest = len.divmod(256**2)
1186 byte1,byte0 = rest.divmod(256)
1188 header << byte2 << byte1 << byte0
1190 elsif self.version =~ /^2\.[34]\./ # 10-byte header
1191 byte3,rest = len.divmod(256**3)
1192 byte2,rest = rest.divmod(256**2)
1193 byte1,byte0 = rest.divmod(256)
1195 flags1,flags0 = self.rawflags.divmod(256)
1197 header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1203 # ----------------------------------------------------------------------
1207 end # of class Frame
1209 # ==============================================================================