id3 0.5.0 → 1.0.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/id3.rb CHANGED
@@ -1,1267 +1,38 @@
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
4
- #
5
- # Copyright (C) 2002 .. 2008 by Tilo Sloboda <tilo@unixgods.org>
6
- #
7
- # created: 12 Oct 2002
8
- # updated: Time-stamp: <Mon 18-Aug-2008 06:16:19 Tilo Sloboda>
9
- #
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
15
- #
16
- # different versions of ID3 tags, support different fields.
17
- # See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3v2_frames_comparison.txt
18
- # See: http://www.unixgods.org/~tilo/Ruby/ID3/docs/ID3_comparison.html
19
- #
20
- # License:
21
- # Freely available under the terms of the OpenSource "Artistic License"
22
- # in combination with the Addendum A (below)
23
- #
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
26
- #
27
- # Addendum A:
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.
31
- #
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.
38
- #
39
- #
40
- # Author's Rant:
41
- # The author of this ID3-library for Ruby is not responsible in any way for
42
- # the definition of the ID3-standards..
43
- #
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
49
- # to implement them..
50
- #
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
56
- #
57
- # If you have some files with valid 2.4.0 tags, please send them my way!
58
- # Thank you!
59
- #
60
- #-------------------------------------------------------------------------------
61
- # Module ID3
62
- #
63
- # Module Functions:
64
- # hasID3v1tag?(filename)
65
- # hasID3v2tag?(filename)
66
- # removeID3v1tag(filename)
67
- #
68
- # Classes:
69
- # AudioFile
70
- # Tag1
71
- # Tag2
72
- # Frame
73
- #
74
- ################################################################################
75
1
 
76
- # ==============================================================================
77
- # Loading other stuff..
78
- # ==============================================================================
79
-
80
- require 'md5'
81
2
  require 'tempfile'
82
- require 'ftools'
83
-
84
- # my extensions:
85
-
86
- require 'hexdump' # load hexdump method to extend class String
87
-
88
- require 'invert_hash' # new invert method for old Hash
89
-
90
-
91
- class Hash # overwrite Hash.invert method
92
- alias old_invert invert
93
-
94
- def invert
95
- self.inverse
96
- end
97
- end
98
-
99
- # ==============================================================================
100
-
101
- module ID3
102
-
103
- # ----------------------------------------------------------------------------
104
- # CONSTANTS
105
- # ----------------------------------------------------------------------------
106
- @@RCSid = '$Id: id3.rb,v 1.2 2004/11/29 05:18:44 tilo Exp tilo $'
107
-
108
- ID3v1tagSize = 128 # ID3v1 and ID3v1.1 have fixed size tags
109
- ID3v1versionbyte = 125
110
- ID3v2headerSize = 10
111
-
112
-
113
- SUPPORTED_SYMBOLS = {
114
- "1.0" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
115
- "YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
116
- # "VERSION"=>"1.0"
117
- } ,
118
- "1.1" => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
119
- "YEAR"=>93..96 , "COMMENT"=>97..124,
120
- "TRACKNUM"=>126, "GENREID"=>127,
121
- # "VERSION"=>"1.1"
122
- } ,
123
-
124
- "2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
125
- "ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
126
- "COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
127
- "ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC",
128
- "DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
129
- "ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT",
130
- "COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN",
131
- "ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
132
- "PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
133
- "ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
134
- "USERTEXT"=>"TXX",
135
- "WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
136
- "WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
137
- "WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
138
- "INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
139
- "CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
140
- "SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
141
- "EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
142
- "GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
143
- "BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
144
- "LINKED"=>"LNK"
145
- } ,
146
-
147
- "2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
148
- "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
149
- "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
150
- "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
151
- "DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
152
- "ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ",
153
- "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
154
- "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
155
- "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
156
- "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
157
- "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
158
- "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
159
- "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
160
- "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
161
- "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
162
- "INVOLVEDPEOPLE"=>"IPLS",
163
- "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
164
- "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
165
- "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
166
- "VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA",
167
- "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
168
- "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
169
- "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
170
- "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
171
- "PRIVATE"=>"PRIV"
172
- } ,
173
-
174
- "2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
175
- "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
176
- "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
177
- "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
178
- "RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
179
- "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
180
- "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
181
- "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
182
- "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
183
- "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
184
- "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
185
- "SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
186
- "ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
187
- "ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
188
- "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
189
- "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
190
- "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
191
- "MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
192
- "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
193
- "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
194
- "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT",
195
- "VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
196
- "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
197
- "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
198
- "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
199
- "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID",
200
- "PRIVATE"=>"PRIV",
201
- "OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
202
- "AUDIOSEEKPOINT"=>"ASPI"
203
- }
204
- }
205
-
206
- # ----------------------------------------------------------------------------
207
- # Flags in the ID3-Tag Header:
208
-
209
- TAG_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
210
- # those flags are supposed to be zero!
211
- "2.2.0" => 0x3F, # 0xC0 ,
212
- "2.3.0" => 0x1F, # 0xE0 ,
213
- "2.4.0" => 0x0F # 0xF0
214
- }
215
-
216
- TAG_HEADER_FLAGS = {
217
- "2.2.0" => { "Unsynchronisation" => 0x80 ,
218
- "Compression" => 0x40 ,
219
- } ,
220
- "2.3.0" => { "Unsynchronisation" => 0x80 ,
221
- "ExtendedHeader" => 0x40 ,
222
- "Experimental" => 0x20 ,
223
- } ,
224
- "2.4.0" => { "Unsynchronisation" => 0x80 ,
225
- "ExtendedHeader" => 0x40 ,
226
- "Experimental" => 0x20 ,
227
- "Footer" => 0x10 ,
228
- }
229
- }
230
-
231
- # ----------------------------------------------------------------------------
232
- # Flags in the ID3-Frame Header:
233
-
234
- FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
235
- # those flags are supposed to be zero!
236
- "2.3.0" => 0x1F1F, # 0xD0D0 ,
237
- "2.4.0" => 0x8FB0 # 0x704F ,
238
- }
239
-
240
- FRAME_HEADER_FLAGS = {
241
- "2.3.0" => { "TagAlterPreservation" => 0x8000 ,
242
- "FileAlterPreservation" => 0x4000 ,
243
- "ReadOnly" => 0x2000 ,
244
-
245
- "Compression" => 0x0080 ,
246
- "Encryption" => 0x0040 ,
247
- "GroupIdentity" => 0x0020 ,
248
- } ,
249
- "2.4.0" => { "TagAlterPreservation" => 0x4000 ,
250
- "FileAlterPreservation" => 0x2000 ,
251
- "ReadOnly" => 0x1000 ,
252
-
253
- "GroupIdentity" => 0x0040 ,
254
- "Compression" => 0x0008 ,
255
- "Encryption" => 0x0004 ,
256
- "Unsynchronisation" => 0x0002 ,
257
- "DataLengthIndicator" => 0x0001 ,
258
- }
259
- }
260
-
261
- # the FrameTypes are not visible to the user - they are just a mechanism
262
- # to define only one parser for multiple FraneNames..
263
- #
264
-
265
- FRAMETYPE2FRAMENAME = {
266
- "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),
267
- "USERTEXT" => "USERTEXT",
268
-
269
- "WEB" => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) ,
270
- "WWWUSER" => "WWWUSER",
271
- "LTEXT" => "TERMSOFUSE" ,
272
- "PICTURE" => "PICTURE" ,
273
- "UNSYNCEDLYRICS" => "UNSYNCEDLYRICS" ,
274
- "COMMENT" => "COMMENT" ,
275
- "BINARY" => %w(PLAYCOUNTER CDID) ,
276
-
277
- # For the following Frames there are no parser stings defined .. the user has access to the raw data
278
- # The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
279
- #
280
- "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)
281
- }
282
-
283
- VARS = 0
284
- PACKING = 1
285
-
286
- # not sure if it's Z* or A*
287
- # A* does not append a \0 when writing!
288
-
289
- # STILL NEED TO GET MORE TEST-CASES! e.g. Japanese ID3-Tags! or other encodings..
290
- # seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
291
-
292
- FRAME_PARSER = {
293
- "TEXT" => [ %w(encoding text) , 'CZ*' ] ,
294
- "USERTEXT" => [ %w(encoding description value) , 'CZ*Z*' ] ,
295
-
296
- "PICTURE" => [ %w(encoding mimeType pictType description picture) , 'CZ*CZ*a*' ] ,
297
-
298
- "WEB" => [ "url" , 'Z*' ] ,
299
- "WWWUSER" => [ %w(encoding description url) , 'CZ*Z*' ] ,
300
-
301
- "LTEXT" => [ %w(encoding language text) , 'CZ*Z*' ] ,
302
- "UNSYNCEDLYRICS" => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
303
- "COMMENT" => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
304
- "BINARY" => [ "binary" , 'a*' ] ,
305
- "UNPARSED" => [ "raw" , 'a*' ] # how would we do value checking for this?
306
- }
307
-
308
- # ----------------------------------------------------------------------------
309
- # MODULE VARIABLES
310
- # ----------------------------------------------------------------------------
311
- Symbol2framename = ID3::SUPPORTED_SYMBOLS
312
- Framename2symbol = Hash.new
313
- Framename2symbol["1.0"] = ID3::SUPPORTED_SYMBOLS["1.0"].invert
314
- Framename2symbol["1.1"] = ID3::SUPPORTED_SYMBOLS["1.1"].invert
315
- Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
316
- Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
317
- Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
318
-
319
- FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
320
-
321
- FrameName2FrameType = FrameType2FrameName.invert
322
-
323
- # ----------------------------------------------------------------------------
324
- # the following piece of code is just for debugging, to sanity-check that all
325
- # the FrameSymbols map back to a FrameType -- otherwise the library code will
326
- # break if we encounter a Frame which can't be mapped to a FrameType..
327
- # ----------------------------------------------------------------------------
328
- #
329
- # ensure we have a FrameType defined for each FrameName, otherwise
330
- # code might break later..
331
- #
332
-
333
- # print "\nMISSING SYMBOLS:\n"
334
-
335
- (ID3::Framename2symbol["2.2.0"].values +
336
- ID3::Framename2symbol["2.3.0"].values +
337
- ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
338
- # print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
339
- print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
340
- }
341
- # print "\n\n"
342
-
343
- # ----------------------------------------------------------------------------
344
- # MODULE FUNCTIONS:
345
- # ----------------------------------------------------------------------------
346
- # The ID3 module functions are to query or modify files directly.
347
- # They check directly if a file has a ID3-tag, but they don't parse the tags!
348
-
349
-
350
- # ----------------------------------------------------------------------------
351
- # hasID3v1tag?
352
- # returns string with version 1.0 or 1.1 if tag was found
353
- # returns false otherwise
354
-
355
- def ID3.hasID3v1tag?(filename)
356
- hasID3v1tag = false
357
-
358
- # be careful with empty or corrupt files..
359
- return false if File.size(filename) < ID3v1tagSize
360
-
361
- f = File.open(filename, 'r')
362
- f.seek(-ID3v1tagSize, IO::SEEK_END)
363
- if (f.read(3) == "TAG")
364
- f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
365
- c = f.getc; # this is character 125 of the tag
366
- if (c == 0)
367
- hasID3v1tag = "1.1"
368
- else
369
- hasID3v1tag = "1.0"
370
- end
371
- end
372
- f.close
373
- return hasID3v1tag
374
- end
375
-
376
- # ----------------------------------------------------------------------------
377
- # hasID3v2tag?
378
- # returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
379
- # returns false otherwise
380
-
381
- def ID3.hasID3v2tag?(filename)
382
- hasID3v2tag = false
383
-
384
- f = File.open(filename, 'r')
385
- if (f.read(3) == "ID3")
386
- major = f.getc
387
- minor = f.getc
388
- version = "2." + major.to_s + '.' + minor.to_s
389
- hasID3v2tag = version
390
- end
391
- f.close
392
- return hasID3v2tag
393
- end
394
-
395
- # ----------------------------------------------------------------------------
396
- # hasID3tag?
397
- # returns string with all versions found, space separated
398
- # returns false otherwise
399
-
400
- def ID3.hasID3tag?(filename)
401
- v1 = ID3.hasID3v1tag?(filename)
402
- v2 = ID3.hasID3v2tag?(filename)
403
-
404
- return false if !v1 && !v2
405
- return v1 if !v2
406
- return v2 if !v1
407
- return "#{v1} #{v2}"
408
- end
409
-
410
- # ----------------------------------------------------------------------------
411
- # removeID3v1tag
412
- # returns nil if no v1 tag was found, or it couldn't be removed
413
- # returns true if v1 tag found and it was removed..
414
- #
415
- # in the future:
416
- # returns ID3.Tag1 object if a v1 tag was found and removed
417
-
418
- def ID3.removeID3v1tag(filename)
419
- stat = File.stat(filename)
420
- if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
421
-
422
- # CAREFUL: this does not check if there really is a valid tag,
423
- # that's why we need to check above!!
424
-
425
- newsize = stat.size - ID3v1tagSize
426
- File.open(filename, "r+") { |f| f.truncate(newsize) }
427
-
428
- return true
429
- else
430
- return nil
431
- end
432
- end
433
- # ----------------------------------------------------------------------------
434
-
435
-
436
- # ==============================================================================
437
- # Class AudioFile may call this ID3File
438
- #
439
- # reads and parses audio files for tags
440
- # writes audio files and attaches dumped tags to it..
441
- # revert feature would be nice to have..
442
- #
443
- # If we query and AudioFile object, we query what's currently associated with it
444
- # e.g. we're not querying the file itself, but the Tag object which is perhaps modified.
445
- # To query the file itself, use the ID3 module functions
446
-
447
- class AudioFile
448
-
449
- attr_reader :audioStartX , :audioEndX # begin and end indices of audio data in file
450
- attr_reader :audioMD5sum # MD5sum of the audio portion of the file
451
-
452
- attr_reader :pwd, :filename # PWD and relative path/name how file was first referenced
453
- attr_reader :dirname, :basename # absolute dirname and basename of the file (computed)
454
-
455
- attr_accessor :tagID3v1, :tagID3v2
456
-
457
- # ----------------------------------------------------------------------------
458
- # initialize
459
- #
460
- # AudioFile.new does NOT keep the file open, but scans it and parses the info
461
-
462
- # e.g.: ID3::AudioFile.new('mp3/a.mp3')
463
-
464
- def initialize(filename)
465
- @filename = filename # similar to path method from class File, which is a mis-nomer!
466
- @pwd = ENV["PWD"]
467
- @dirname = File.dirname( "#{@pwd}/#{@filename}" ) # just sugar
468
- @basename = File.basename( "#{@pwd}/#{@filename}" ) # just sugar
469
-
470
- @tagID3v1 = nil
471
- @tagID3v2 = nil
472
-
473
- audioStartX = 0
474
- audioEndX = File.size(filename) - 1 # points to the last index
475
-
476
- if ID3.hasID3v1tag?(@filename)
477
- @tagID3v1 = Tag1.new
478
- @tagID3v1.read(@filename)
479
-
480
- audioEndX -= ID3::ID3v1tagSize
481
- end
482
- if ID3.hasID3v2tag?(@filename)
483
- @tagID3v2 = Tag2.new
484
- @tagID3v2.read(@filename)
485
-
486
- audioStartX = @tagID3v2.raw.size
487
- end
488
-
489
- # audioStartX audioEndX indices into the file need to be set
490
- @audioStartX = audioStartX # first byte of audio data
491
- @audioEndX = audioEndX # last byte of audio data
492
-
493
- # user may compute the MD5sum of the audio content later..
494
- # but we're only doing this if the user requests it..
495
- # because MD5sum computation takes a little bit time.
496
-
497
- @audioMD5sum = nil
498
- end
499
-
500
- # ----------------------------------------------------------------------------
501
- def audioLength
502
- @audioEndX - @audioStartX + 1
503
- end
504
- # ----------------------------------------------------------------------------
505
- # write
506
- # write the AudioFile to file, including any ID3-tags
507
- # We keep backups if we write to a specific filename
508
-
509
- def write(*filename)
510
- backups = false
511
-
512
- if filename.size == 0 # this is an Array!!
513
- filename = @filename
514
- backups = true # keep backups if we write to a specific filename
515
- else
516
- filename = filename[0]
517
- backups = false
518
- end
519
-
520
- tf = Tempfile.new( @basename )
521
- tmpname = tf.path
522
-
523
- # write ID3v2 tag:
524
-
525
- if @tagID3v2
526
- tf.write( @tagID3v2.dump )
527
- end
528
-
529
- # write Audio Data:
530
-
531
- File.open( @filename ) { |f|
532
- f.seek(@audioStartX)
533
- tf.write( f.read(@audioEndX - @audioStartX +1) )
534
- }
535
-
536
- # write ID3v1 tag:
537
-
538
- if @tagID3v1
539
- tf.write( @tagID3v1.dump )
540
- end
541
-
542
- tf.close
543
-
544
- # now some logic about moving the tempfile and replacing the original
3
+ require 'active_support' # we'll borrow OrdreedHash from here.. no need to reinvent the wheel
545
4
 
546
- bakname = filename + '.bak'
547
- File.move(filename, bakname) if backups && FileTest.exists?(filename) && ! FileTest.exists?(bakname)
548
5
 
549
- File.move(tmpname, filename)
550
- tf.close(true)
551
-
552
- # write md5sum file:
553
-
554
- writeMD5sum if @audioMD5sum
6
+ require 'helpers/ruby_1.8_1.9_compatibility' # define helper methods, so that we have the same interface for Ruby 1.8 and 1.9
555
7
 
556
- end
557
-
558
- # ----------------------------------------------------------------------------
559
- # writeAudio
560
- # only for debugging, does not write any ID3-tags, but just the audio portion
561
-
562
- def writeAudio
563
- tf = Tempfile.new( @basename )
564
-
565
- File.open( @filename ) { |f|
566
- f.seek(@audioStartX)
567
- tf.write( f.read(@audioEndX - @audioStartX + 1) )
568
- }
569
- tf.close
570
- path = tf.path
571
-
572
- tf.open
573
- tf.close(true)
574
- end
575
-
576
-
577
- # ----------------------------------------------------------------------------
578
- # NOTE on md5sum's:
579
- # If you don't know what an md5sum is, you can think of it as a unique
580
- # fingerprint of a file or some data. I added the md5sum computation to
581
- # help users keep track of their converted songs - even if the ID3-tag of
582
- # a file changes, the md5sum of the audio data does not change..
583
- # The md5sum can help you ensure that the audio-portion of the file
584
- # was not changed after modifying, adding or deleting ID3-tags.
585
-
586
- # ----------------------------------------------------------------------------
587
- # audioMD5sum
588
- # if the user tries to access @audioMD5sum, it will be computed for him,
589
- # unless it was previously computed. We try to calculate that only once
590
- # and on demand, because it's a bit expensive to compute..
591
-
592
- def audioMD5sum
593
- if ! @audioMD5sum
594
-
595
- File.open( File.join(@dirname,@basename) ) { |f|
596
- f.seek(@audioStartX)
597
- @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
598
- }
8
+ require 'helpers/hash_extensions' # loads Hash#inverse and overloads Hash#invert
9
+ require 'helpers/restricted_ordered_hash' # derived from OrderedHash; used throughout ID3 library
599
10
 
600
- end
601
- @audioMD5sum
602
- end
603
- # ----------------------------------------------------------------------------
604
- # writeMD5sum
605
- # write the filename and MD5sum of the audio portion into an ascii file
606
- # in the same location as the audio file, but with suffix .md5
607
- #
608
- # computes the @audioMD5sum, if it wasn't previously computed..
11
+ # load hexdump method to extend class String
12
+ require "helpers/hexdump" # only needed for debugging -> autoload
609
13
 
610
- def writeMD5sum
611
-
612
- self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
613
-
614
- base = @basename.sub( /(.)\.[^.]+$/ , '\1')
615
- base += '.md5'
616
- File.open( File.join(@dirname,base) ,"w") { |f|
617
- f.printf("%s %s\n", File.join(@dirname,@basename), @audioMD5sum)
618
- }
619
- @audioMD5sum
620
- end
621
- # ----------------------------------------------------------------------------
622
- # verifyMD5sum
623
- # compare the audioMD5sum against a previously stored md5sum file
624
- # and returns boolean value of comparison
625
- #
626
- # If no md5sum file existed, we create one and return true.
627
- #
628
- # computes the @audioMD5sum, if it wasn't previously computed..
14
+ require 'id3/string_extensions' # adds ID3 methods to String
15
+ require 'id3/io_extensions' # adds ID3 methods to IO and File
629
16
 
630
- def verifyMD5sum
17
+ require 'id3/constants' # Constants used throughout the code
18
+ require 'id3/module_methods' # add ID3 methods to ID3 which operate on filenames
631
19
 
632
- oldMD5sum = ''
633
-
634
- self.audioMD5sum if ! @audioMD5sum # compute MD5sum if it's not computed yet
20
+ require 'id3/audiofile' # Higher-Level access to Audio Files with ID3 tags
635
21
 
636
- base = @basename.sub( /(.)\.[^.]+$/ , '\1') # remove suffix from audio-file
637
- base += '.md5' # add new suffix .md5
638
- md5name = File.join(@dirname,base)
639
-
640
- # if a MD5-file doesn't exist, we should create one and return TRUE ...
641
- if File.exists?(md5name)
642
- File.open( md5name ,"r") { |f|
643
- oldname,oldMD5sum = f.readline.split # read old MD5-sum
644
- }
645
- else
646
- oldMD5sum = self.writeMD5sum # create MD5-file and return true..
647
- end
648
- @audioMD5sum == oldMD5sum
649
-
650
- end
651
- # ----------------------------------------------------------------------------
652
- # version aka versions
653
- # queries the tag objects and returns the version numbers of those tags
654
- # NOTE: this does not reflect what's currently in the file, but what's
655
- # currently in the AudioFile object
656
-
657
- def version
658
- a = Array.new
659
- a.push(@tagID3v1.version) if @tagID3v1
660
- a.push(@tagID3v2.version) if @tagID3v2
661
- return nil if a == []
662
- a.join(' ')
663
- end
664
- alias versions version
665
- # ----------------------------------------------------------------------------
666
22
 
667
-
668
-
669
- end # of class AudioFile
23
+ require 'id3/generic_tag'
24
+ require 'id3/tag1' # ID3v1 tag class
25
+ require 'id3/tag2' # ID3v2 tag class
26
+ require 'id3/frame' # ID3v2 frame class
27
+ require 'id3/frame_array' # Array extension for arrays of ID3::Frame s
670
28
 
671
-
672
- # ==============================================================================
673
- # Class RestrictedOrderedHash
674
- # this is a helper Class for ID3::Frame
675
- #
676
-
677
- class RestrictedOrderedHash < Hash
678
29
 
679
- attr_accessor :count , :order, :locked
680
-
681
- def lock
682
- @locked = true
683
- end
684
-
685
- def initialize
686
- @locked = false
687
- @count = 0
688
- @order = []
689
- super
690
- end
691
-
692
- alias old_store []=
693
-
694
- def []= (key,val)
695
- if self[key]
696
- self.old_store(key,val)
697
- else
698
- if @locked
699
- # we're not allowed to add new keys!
700
- raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
701
- " valid key are: " + self.keys.join(",") +"\n"
702
-
703
- else
704
- @count += 1
705
- @order += [key]
706
- self.old_store(key,val)
707
- end
708
- end
709
- end
710
-
711
- def values
712
- array = []
713
- @order.each { |key|
714
- array.push self[key]
715
- }
716
- array
717
- end
718
-
719
- # returns the human-readable ordered hash in correct order .. ;-)
720
-
721
- def inspect
722
- first = true
723
- str = "{"
724
- self.order.each{ |key|
725
- str += ", " if !first
726
- str += key.inspect
727
- str += "=>"
728
- str += (self[key]).inspect
729
- first = false
730
- }
731
- str +="}"
732
- end
733
-
734
- # users can not delete entries from a locked hash..
735
-
736
- alias old_delete delete
737
-
738
- def delete (key)
739
- if !@locked
740
- old_delete(key)
741
- @order.delete(key)
742
- end
743
- end
744
-
745
- end
746
-
747
-
748
-
749
- # ==============================================================================
750
- # Class GenericTag
751
- #
752
- # Helper class for Tag1 and Tag2
753
- #
754
- # Checks that user uses a valid key, and adds methods for size computation
755
- #
756
- # as per ID3-definition, the frames are in no fixed order! that's why we can derive
757
- # this class from Hash. But in the future we may want to write certain frames first
758
- # into the ID3-tag and therefore may want to derive it from RestrictedOrderedHash
759
-
760
- class GenericTag < Hash
761
- attr_reader :version, :raw
762
-
763
- # these definitions are to prevent users from inventing their own field names..
764
- # but on the other hand, they should be able to create a new valid field, if
765
- # it's not yet in the current tag, but it's valid for that ID3-version...
766
-
767
- alias old_set []=
768
- private :old_set
30
+ # module ID3
769
31
 
770
- # ----------------------------------------------------------------------
771
- def []=(key,val)
772
- if @version == ""
773
- raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n"
774
- else
775
- if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
776
- old_set(key,val)
777
- else
778
- # exception
779
- raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
780
- " valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
781
- end
782
- end
783
- end
784
- # ----------------------------------------------------------------------
785
- # convert the 4 bytes found in the id3v2 header and return the size
786
- private
787
- def unmungeSize(bytes)
788
- size = 0
789
- j = 0; i = 3
790
- while i >= 0
791
- size += 128**i * (bytes[j] & 0x7f)
792
- j += 1
793
- i -= 1
794
- end
795
- return size
796
- end
797
- # ----------------------------------------------------------------------
798
- # convert the size into 4 bytes to be written into an id3v2 header
799
- private
800
- def mungeSize(size)
801
- bytes = Array.new(4,0)
802
- j = 0; i = 3
803
- while i >= 0
804
- bytes[j],size = size.divmod(128**i)
805
- j += 1
806
- i -= 1
807
- end
808
-
809
- return bytes
810
- end
811
- # ----------------------------------------------------------------------------
812
-
813
- end # of class GenericTag
814
-
815
- # ==============================================================================
816
- # Class Tag1 ID3 Version 1.x Tag
817
- #
818
- # parses ID3v1 tags from a binary array
819
- # dumps ID3v1 tags into a binary array
820
- # allows to modify tag's contents
821
-
822
- class Tag1 < GenericTag
823
-
824
- # ----------------------------------------------------------------------
825
- # read reads a version 1.x ID3tag
826
- #
827
-
828
- def read(filename)
829
- f = File.open(filename, 'r')
830
- f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
831
- hastag = (f.read(3) == 'TAG')
832
- if hastag
833
- f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
834
- @raw = f.read(ID3::ID3v1tagSize)
835
-
836
- # self.parse!(raw) # we should use "parse!" instead of duplicating code!
837
-
838
- if (raw[ID3v1versionbyte] == 0)
839
- @version = "1.1"
840
- else
841
- @version = "1.0"
842
- end
843
- else
844
- @raw = @version = nil
845
- end
846
- f.close
847
- #
848
- # now parse all the fields
849
-
850
- ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
851
- if val.class == Range
852
- self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
853
- elsif val.class == Fixnum
854
- self[key] = @raw[val].to_s
855
- else
856
- # this can't happen the way we defined the hash..
857
- # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.type
858
- end
859
- }
860
- hastag
861
- end
862
- # ----------------------------------------------------------------------
863
- # write writes a version 1.x ID3tag
864
- #
865
- # not implemented yet..
866
- #
867
- # need to loacte old tag, and remove it, then append new tag..
868
- #
869
- # always upgrade version 1.0 to 1.1 when writing
870
-
871
- # not yet implemented, because AudioFile.write does the job better
872
-
873
- # ----------------------------------------------------------------------
874
- # this routine modifies self, e.g. the Tag1 object
875
- #
876
- # tag.parse!(raw) returns boolean value, showing if parsing was successful
877
-
878
- def parse!(raw)
879
-
880
- return false if raw.size != ID3::ID3v1tagSize
881
-
882
- if (raw[ID3v1versionbyte] == 0)
883
- @version = "1.1"
884
- else
885
- @version = "1.0"
886
- end
887
-
888
- self.clear # remove all entries from Hash, we don't want left-overs..
889
-
890
- ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
891
- if val.class == Range
892
- self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
893
- elsif val.class == Fixnum
894
- self[key] = raw[val].to_s
895
- else
896
- # this can't happen the way we defined the hash..
897
- # printf "unknown key/val : #{key} / #{val} ; val-type: %s\n", val.class
898
- end
899
- }
900
- @raw = raw
901
- return true
902
- end
903
- # ----------------------------------------------------------------------
904
- # dump version 1.1 ID3 Tag into a binary array
905
- #
906
- # although we provide this method, it's stongly discouraged to use it,
907
- # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
908
- # are often truncated and hence ID3 v1 tags are often useless..
909
-
910
- def dump
911
- zeroes = "\0" * 32
912
- raw = "\0" * ID3::ID3v1tagSize
913
- raw[0..2] = 'TAG'
914
-
915
- self.each{ |key,value|
916
-
917
- range = ID3::Symbol2framename['1.1'][key]
918
-
919
- if range.class == Range
920
- length = range.last - range.first + 1
921
- paddedstring = value + zeroes
922
- raw[range] = paddedstring[0..length-1]
923
- elsif range.class == Fixnum
924
- raw[range] = value.to_i
925
- else
926
- # this can't happen the way we defined the hash..
927
- next
928
- end
929
- }
930
-
931
- return raw
932
- end
933
- # ----------------------------------------------------------------------
934
- end # of class Tag1
935
-
936
- # ==============================================================================
937
- # Class Tag2 ID3 Version 2.x.y Tag
938
- #
939
- # parses ID3v2 tags from a binary array
940
- # dumps ID3v2 tags into a binary array
941
- # allows to modify tag's contents
942
- #
943
- # as per definition, the frames are in no fixed order
944
-
945
- class Tag2 < GenericTag
946
-
947
- attr_reader :rawflags, :flags
948
-
949
- def initalize
950
- @rawflags = 0
951
- @flags = {}
952
- super
953
- end
954
-
955
- def read(filename)
956
- f = File.open(filename, 'r')
957
- hastag = (f.read(3) == "ID3")
958
- if hastag
959
- major = f.getc
960
- minor = f.getc
961
- @version = "2." + major.to_s + '.' + minor.to_s
962
- @rawflags = f.getc
963
- size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
964
- f.seek(0)
965
- @raw = f.read(size)
966
-
967
- # parse the raw flags:
968
- if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
969
- # in this case we need to skip parsing the frame... and skip to the next one...
970
- wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
971
- error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
972
- raise ArgumentError, error
973
- end
974
-
975
- @flags = Hash.new
976
-
977
- TAG_HEADER_FLAGS[@version].each{ |key,val|
978
- # only define the flags which are set..
979
- @flags[key] = true if (@rawflags & val == 1)
980
- }
981
-
982
-
983
- else
984
- @raw = nil
985
- @version = nil
986
- return false
987
- end
988
- f.close
989
- #
990
- # now parse all the frames
991
- #
992
- i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
993
-
994
- while (i < @raw.size) && (@raw[i] != 0)
995
- len,frame = parse_frame_header(i) # this will create the correct frame
996
- if len != 0
997
- i += len
998
- else
999
- break
1000
- end
1001
- end
1002
-
1003
- hastag
1004
- end
1005
-
1006
- # ----------------------------------------------------------------------
1007
- # write
1008
- #
1009
- # writes and replaces existing ID3-v2-tag if one is present
1010
- # Careful, this does NOT merge or append, it overwrites!
1011
-
1012
- # not yet implemented, because AudioFile.write does the job better
1013
-
1014
- # def write(filename)
1015
- # check how long the old ID3-v2 tag is
1016
-
1017
- # dump ID3-v2-tag
1018
-
1019
- # append old audio to new tag
1020
-
1021
- # end
1022
-
1023
- # ----------------------------------------------------------------------------
1024
- # writeID3v2
1025
- # just writes the ID3v2 tag by itself into a file, no audio data is written
1026
- #
1027
- # for backing up ID3v2 tags and debugging only..
1028
- #
1029
-
1030
- # def writeID3v2
1031
-
1032
- # end
1033
-
1034
- # ----------------------------------------------------------------------
1035
- # parse_frame_header
1036
- #
1037
- # each frame consists of a header of fixed length;
1038
- # depending on the ID3version, either 6 or 10 bytes.
1039
- # and of a data portion which is of variable length,
1040
- # and which contents might not be parsable by us
1041
- #
1042
- # INPUT: index to where in the @raw data the frame starts
1043
- # RETURNS: if successful parse:
1044
- # total size in bytes, ID3frame struct
1045
- # else:
1046
- # 0, nil
1047
- #
1048
- #
1049
- # Struct of type ID3frame which contains:
1050
- # the name, size (in bytes), headerX,
1051
- # dataStartX, dataEndX, flags
1052
- # the data indices point into the @raw data, so we can cut out
1053
- # and parse the data at a later point in time.
1054
- #
1055
- # total frame size = dataEndX - headerX
1056
- # total header size= dataStartX - headerX
1057
- # total data size = dataEndX - dataStartX
1058
- #
1059
- private
1060
- def parse_frame_header(x)
1061
- framename = ""; flags = nil
1062
- size = 0
1063
-
1064
- if @version =~ /^2\.2\./
1065
- frameHeaderSize = 6 # 2.2.x Header Size is 6 bytes
1066
- header = @raw[x..x+frameHeaderSize-1]
1067
-
1068
- framename = header[0..2]
1069
- size = (header[3]*256**2)+(header[4]*256)+header[5]
1070
- flags = nil
1071
- # printf "frame: %s , size: %d\n", framename , size
1072
-
1073
- elsif @version =~ /^2\.[34]\./
1074
- # for version 2.3.0 and 2.4.0 the header is 10 bytes long
1075
- frameHeaderSize = 10
1076
- header = @raw[x..x+frameHeaderSize-1]
1077
-
1078
- framename = header[0..3]
1079
- size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
1080
- flags= header[8..9]
1081
- # printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
1082
-
1083
- else
1084
- # we can't parse higher versions
1085
- return 0, false
1086
- end
1087
-
1088
- # if this is a valid frame of known type, we return it's total length and a struct
1089
- #
1090
- if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
1091
- frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
1092
- self[ Framename2symbol[@version][frame.name] ] = frame
1093
- return size+frameHeaderSize , frame
1094
- else
1095
- return 0, nil
1096
- end
1097
- end
1098
- # ----------------------------------------------------------------------
1099
- # dump a ID3-v2 tag into a binary array
1100
- #
1101
- # NOTE:
1102
- # when "dumping" an ID3-v2 tag, I would like to have more control about
1103
- # which frames get dumped first.. e.g. the most important frames (with the
1104
- # most important information) should be dumped first..
1105
- #
1106
-
1107
- public
1108
- def dump
1109
- data = ""
1110
-
1111
- # dump all the frames
1112
- self.each { |framename,framedata|
1113
- data << framedata.dump
1114
- }
1115
- # add some padding perhaps 32 bytes (should be defined by the user!)
1116
- # NOTE: I noticed that iTunes adds excessive amounts of padding
1117
- data << "\0" * 32
1118
-
1119
- # calculate the complete length of the data-section
1120
- size = mungeSize(data.size)
1121
-
1122
- major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1123
-
1124
- # prepend a valid ID3-v2.x header to the data block
1125
- header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1126
-
1127
- header + data
1128
- end
1129
- # ----------------------------------------------------------------------
1130
-
1131
- end # of class Tag2
1132
-
1133
- # ==============================================================================
1134
- # Class Frame ID3 Version 2.x.y Frame
1135
- #
1136
- # parses ID3v2 frames from a binary array
1137
- # dumps ID3v2 frames into a binary array
1138
- # allows to modify frame's contents if the frame was decoded..
1139
- #
1140
-
1141
- class Frame < RestrictedOrderedHash
1142
-
1143
- attr_reader :name, :version
1144
- attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader # debugging only
1145
-
1146
- # ----------------------------------------------------------------------
1147
- # return the complete raw frame
1148
-
1149
- def raw
1150
- return @rawheader + @rawdata
1151
- end
1152
- # ----------------------------------------------------------------------
1153
- alias old_init initialize
1154
-
1155
- def initialize(tag, name, headerStartX, dataStartX, dataEndX, flags)
1156
- @name = name
1157
- @headerStartX = headerStartX
1158
- @dataStartX = dataStartX
1159
- @dataEndX = dataEndX
1160
-
1161
- @rawdata = tag.raw[dataStartX..dataEndX]
1162
- @rawheader = tag.raw[headerStartX..dataStartX-1]
1163
-
1164
- # initialize the super class..
1165
- old_init
1166
-
1167
- # parse the darn flags, if there are any..
1168
-
1169
- @version = tag.version # caching..
1170
- case @version
1171
- when /2\.2\.[0-9]/
1172
- # no flags, no extra attributes necessary
1173
-
1174
- when /2\.[34]\.0/
1175
-
1176
- # dynamically create attributes and reader functions:
1177
- instance_eval <<-EOB
1178
- class << self
1179
- attr_reader :rawflags, :flags
1180
- end
1181
- EOB
1182
-
1183
- @rawflags = flags.to_i # preserve the raw flags (for debugging only)
1184
-
1185
- if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1186
- # in this case we need to skip parsing the frame... and skip to the next one...
1187
- wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1188
- error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1189
- raise ArgumentError, error
1190
- end
1191
-
1192
- @flags = Hash.new
1193
-
1194
- FRAME_HEADER_FLAGS[@version].each{ |key,val|
1195
- # only define the flags which are set..
1196
- @flags[key] = true if (flags.to_i & val == 1)
1197
- }
1198
-
1199
- else
1200
- raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1201
- end # parsing flags
1202
-
1203
- # generate method for parsing data
1204
-
1205
- instance_eval <<-EOB
1206
- class << self
1207
-
1208
- def parse
1209
- # here we GENERATE the code to parse, dump and verify methods
1210
-
1211
- vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1212
-
1213
- # debugging print-out:
1214
-
1215
- if vars.class == Array
1216
- vars2 = vars.join(",")
1217
- else
1218
- vars2 = vars
1219
- end
1220
-
1221
- values = self.rawdata.unpack(packing)
1222
- vars.each { |key|
1223
- self[key] = values.shift
1224
- }
1225
- self.lock # lock the OrderedHash
1226
- end
1227
-
1228
- def dump
1229
- vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1230
-
1231
- data = self.values.pack(packing) # we depend on an OrderedHash, so the values are in the correct order!!!
1232
- header = self.name.dup # we want the value! not the reference!!
1233
- len = data.length
1234
- if self.version =~ /^2\.2\./
1235
- byte2,rest = len.divmod(256**2)
1236
- byte1,byte0 = rest.divmod(256)
1237
-
1238
- header << byte2 << byte1 << byte0
1239
-
1240
- elsif self.version =~ /^2\.[34]\./ # 10-byte header
1241
- byte3,rest = len.divmod(256**3)
1242
- byte2,rest = rest.divmod(256**2)
1243
- byte1,byte0 = rest.divmod(256)
1244
-
1245
- flags1,flags0 = self.rawflags.divmod(256)
1246
-
1247
- header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1248
- end
1249
- header << data
1250
- end
1251
- end
1252
- EOB
1253
- self.parse # now we're using the just defined parsing routine
1254
-
1255
- self
1256
- end
1257
- # ----------------------------------------------------------------------
1258
-
1259
-
1260
-
1261
- end # of class Frame
1262
-
1263
- # ==============================================================================
1264
-
1265
-
32
+ # autoload :AudioFile , 'id3/audio_file.rb'
33
+ # autoload :GenericTag, 'id3/tag_generic.rb'
34
+ # autoload :Tag1 , 'id3/tag1.rb'
35
+ # autoload :Tag2 , 'id3/tag2.rb'
36
+ # autoload :Frame , 'id3/frame.rb'
1266
37
 
1267
- end # of module ID3
38
+ # end