ruby-mp3info 0.4 → 0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG ADDED
@@ -0,0 +1,61 @@
1
+ [0.5 06/12/2005]
2
+
3
+ * id3v2 writing and removing support added. tag2 attribute is r/w now
4
+ * max guess size to find a valid frame set to 2Mb
5
+ * implemented a new class ID3v2, ID2TAGS moved into it
6
+ * Mp3Info.tag is r/w now and has priority over @tag1 and @tag2 when writing
7
+ * added Mp3Info#rename() method to change the filename written at close
8
+ * clean up: all overloaded standards classes replaced by including modules
9
+ * FIXED bug in reading id3v2 tags tagged with olds versions of "mp3ext" ( http://www.mutschler.de/mp3ext/ )
10
+ * FIXED bug on calculating id3v2 frame size
11
+ * FIXED bug when multiple TLEN tags
12
+ * FIXED bug when converting text tag from Unicode
13
+ * FIXED bug: file was not closed, causing too many opened files and test failure on win32
14
+
15
+
16
+ [0.4 26/04/2005]
17
+
18
+ * fixes in vbr mode
19
+ * removed extract_info_from_head() function
20
+ * now try several times to find a good header frame before giving up
21
+ * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
22
+ * FIXED if a tag appears more than one time, create an array with every value found for this tag
23
+
24
+
25
+ [0.3 04/05/2004]
26
+
27
+ * massive changes of most of the code to make it easier to read & hopefully run faster
28
+ * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
29
+ * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
30
+ * much improved vbr duration guessing
31
+ * made Mp3Info#to_s output to be prettier
32
+ * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
33
+ * fixed a bug on computing "error_protection" attribute
34
+ * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
35
+ * new method hastag?, which test the presence of any tag
36
+
37
+
38
+ [0.2.1 04/09/2003]
39
+
40
+ * filename attribute added
41
+ * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
42
+ * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
43
+ * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
44
+ * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
45
+ * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
46
+
47
+
48
+ [0.2 18/08/2003]
49
+
50
+ * writing, reading and removing of id3v1 tags
51
+ * reading of id3v2 tags
52
+ * test suite improved
53
+ * to_s method added
54
+ * length attribute is a Float now
55
+
56
+
57
+ [0.1 17/03/2003]
58
+
59
+ * Initial version
60
+
61
+
data/EXAMPLES ADDED
@@ -0,0 +1,40 @@
1
+ = Examples
2
+
3
+ require "mp3info"
4
+
5
+ # read and display infos & tags
6
+
7
+ Mp3Info.open("myfile.mp3") do |mp3info|
8
+ puts mp3info
9
+ end
10
+
11
+
12
+
13
+ # read/write tag1 and tag2 with Mp3Info#tag attribute
14
+ # when reading tag2 have priority over tag1
15
+ # when writing, each tag is written.
16
+
17
+
18
+ Mp3Info.open("myfile.mp3") do |mp3|
19
+ puts mp3.tag.title
20
+ puts mp3.tag.artist
21
+ puts mp3.tag.album
22
+ puts mp3.tag.tracknum
23
+
24
+ mp3.tag.title = "track title"
25
+ mp3.tag.artist = "artist name"
26
+
27
+ end
28
+
29
+ Mp3Info.open("myfile.mp3") do |mp3|
30
+ # you can access four letter v2 tags like this
31
+ puts mp3.tag2.TIT2
32
+ mp3.tag2.TIT2 = "new TIT2"
33
+ # or like that
34
+ mp3.tag2["TIT2"]
35
+
36
+ # at this time, only COMM tag is processed after reading and before writing
37
+ # according to ID3v2#options hash
38
+ mp3.tag2.options[:lang] = "FRE"
39
+ mp3.tag2.COMM = "my comment in french, correctly handled when reading and writing"
40
+ end
data/README ADDED
@@ -0,0 +1,28 @@
1
+ = Description
2
+ ruby-mp3info gives you access to low level informations on mp3 files
3
+ (bitrate, length, samplerate, etc...). It can read, write, remove id3v1 and
4
+ id3v2 tags. It is written in pure ruby.
5
+
6
+
7
+ = Download
8
+
9
+ get tar.gz at
10
+ http://rubyforge.org/projects/ruby-mp3info/
11
+
12
+
13
+ = Installation
14
+
15
+ $ ruby install.rb config
16
+ $ ruby install.rb setup
17
+ # ruby install.rb install
18
+
19
+ or
20
+
21
+ # gem install ruby-mp3info
22
+
23
+
24
+ = Todo
25
+
26
+ * encoder detection
27
+ * support for more tags in id3v2
28
+ * generalize id3v2 with other audio formats (APE, MPC, OGG, etc...)
data/lib/mp3info.rb CHANGED
@@ -1,143 +1,24 @@
1
1
  # $Id: mp3info.rb,v 1.5 2005/04/26 13:41:41 moumar Exp $
2
- # = Description
3
- #
4
- # ruby-mp3info gives you access to low level informations on mp3 files
5
- # (bitrate, length, samplerate, etc...). It can read, write, remove id3v1 tag
6
- # and read id3v2. It is written in pure ruby.
7
- #
8
- #
9
- # = Download
10
- #
11
- # get tar.gz at
12
- # http://rubyforge.org/projects/ruby-mp3info/
13
- #
14
- #
15
- # = Installation
16
- #
17
- # $ ruby install.rb config
18
- # $ ruby install.rb setup
19
- # # ruby install.rb install
20
- #
21
- # or
22
- #
23
- # # gem install ruby-mp3info
24
- #
25
- #
26
- # = Example
27
- #
28
- # require "mp3info"
29
- #
30
- # mp3info = Mp3Info.new("myfile.mp3")
31
- # puts mp3info
32
- #
33
- #
34
- # = Testing
35
- #
36
- # Test::Unit library is used for tests. see http://testunit.talbott.ws/
37
- #
38
- # $ ruby test.rb
39
- #
40
- #
41
- # = ToDo
42
- #
43
- # * adding write support for ID3v2 tags
44
- # * adding a test for id3v2
45
- # * encoder detection
46
- #
47
- #
48
- # = Changelog
49
- #
50
- # [0.4 26/04/2005]
51
- #
52
- # * fixes in vbr mode
53
- # * removed extract_info_from_head() function
54
- # * now try several times to find a good header frame before giving up
55
- # * correct handling of unicode in v2 tags. Require standard "iconv" library if such tags are used
56
- # * FIXED if a tag appears more than one time, create an array with every value found for this tag
57
- #
58
- #
59
- # [0.3 04/05/2004]
60
- #
61
- # * massive changes of most of the code to make it easier to read & hopefully run faster
62
- # * ID2TAGS hash is just informative now, no use of it in the code. id3v2 tag fields are read in directly
63
- # * added support for id3 v2.2 and v2.4 (0.2.1 only supported v2.3)
64
- # * much improved vbr duration guessing
65
- # * made Mp3Info#to_s output to be prettier
66
- # * moved hastag1? and hastag2? to be class booleans instead of functions (now named hastag1 and hastag2)
67
- # * fixed a bug on computing "error_protection" attribute
68
- # * new attribute "tag", which is a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
69
- # * new method hastag?, which test the presence of any tag
70
- #
71
- #
72
- # [0.2.1 04/09/2003]
73
- #
74
- # * filename attribute added
75
- # * mp3 files are opened read-only now [Alan Davies <alan__DOT_davies__AT__thomson.com>]
76
- # * Mp3Info#initialize: bugfixes [Alan Davies <alan__DOT_davies__AT__thomson.com>]
77
- # * put NULLs in year field in id3v1 tags instead of zeros [Alan Davies <alan__DOT_davies__AT__thomson.com>]
78
- # * Mp3Info#gettag1: remove null at end of strings [Alan Davies <alan__DOT_davies__AT__thomson.com>]
79
- # * Mp3Info#extract_infos_from_head(): some brackets missed [Alan Davies <alan__DOT_davies__AT__thomson.com>]
80
- #
81
- #
82
- # [0.2 18/08/2003]
83
- #
84
- # * writing, reading and removing of id3v1 tags
85
- # * reading of id3v2 tags
86
- # * test suite improved
87
- # * to_s method added
88
- # * length attribute is a Float now
89
- #
90
- #
91
- # [0.1 17/03/2003]
92
- #
93
- # * Initial version
94
- #
95
- #
96
2
  # License:: Ruby
97
3
  # Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
98
4
  # Website:: http://ruby-mp3info.rubyforge.org/
99
5
 
6
+ require "delegate"
7
+ require "fileutils"
8
+ require "mp3info/extension_modules"
9
+ require "mp3info/id3v2"
10
+
11
+ # ruby -d to display debugging infos
12
+
100
13
  # Raised on any kind of error related to ruby-mp3info
101
14
  class Mp3InfoError < StandardError ; end
102
15
 
103
16
  class Mp3InfoInternalError < StandardError #:nodoc:
104
17
  end
105
18
 
106
- class Numeric
107
- ### returns the selected bit range (b, a) as a number
108
- ### NOTE: b > a if not, returns 0
109
- def bits(b, a)
110
- t = 0
111
- b.downto(a) { |i| t += t + self[i] }
112
- t
113
- end
114
- end
115
-
116
- class Hash
117
- ### lets you specify hash["key"] as hash.key
118
- ### this came from CodingInRuby on RubyGarden
119
- ### http://www.rubygarden.org/ruby?CodingInRuby
120
- def method_missing(meth,*args)
121
- if /=$/=~(meth=meth.id2name) then
122
- self[meth[0...-1]] = (args.length<2 ? args[0] : args)
123
- else
124
- self[meth]
125
- end
126
- end
127
- end
128
-
129
- class File
130
- def get32bits
131
- (getc << 24) + (getc << 16) + (getc << 8) + getc
132
- end
133
- def get_syncsafe
134
- (getc << 21) + (getc << 14) + (getc << 7) + getc
135
- end
136
- end
137
-
138
19
  class Mp3Info
139
20
 
140
- VERSION = "0.4"
21
+ VERSION = "0.5"
141
22
 
142
23
  LAYER = [ nil, 3, 2, 1]
143
24
  BITRATE = [
@@ -185,86 +66,18 @@ class Mp3Info
185
66
  "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
186
67
  "SynthPop" ]
187
68
 
188
-
189
- ID2TAGS = {
190
- "AENC" => "Audio encryption",
191
- "APIC" => "Attached picture",
192
- "COMM" => "Comments",
193
- "COMR" => "Commercial frame",
194
- "ENCR" => "Encryption method registration",
195
- "EQUA" => "Equalization",
196
- "ETCO" => "Event timing codes",
197
- "GEOB" => "General encapsulated object",
198
- "GRID" => "Group identification registration",
199
- "IPLS" => "Involved people list",
200
- "LINK" => "Linked information",
201
- "MCDI" => "Music CD identifier",
202
- "MLLT" => "MPEG location lookup table",
203
- "OWNE" => "Ownership frame",
204
- "PRIV" => "Private frame",
205
- "PCNT" => "Play counter",
206
- "POPM" => "Popularimeter",
207
- "POSS" => "Position synchronisation frame",
208
- "RBUF" => "Recommended buffer size",
209
- "RVAD" => "Relative volume adjustment",
210
- "RVRB" => "Reverb",
211
- "SYLT" => "Synchronized lyric/text",
212
- "SYTC" => "Synchronized tempo codes",
213
- "TALB" => "Album/Movie/Show title",
214
- "TBPM" => "BPM (beats per minute)",
215
- "TCOM" => "Composer",
216
- "TCON" => "Content type",
217
- "TCOP" => "Copyright message",
218
- "TDAT" => "Date",
219
- "TDLY" => "Playlist delay",
220
- "TENC" => "Encoded by",
221
- "TEXT" => "Lyricist/Text writer",
222
- "TFLT" => "File type",
223
- "TIME" => "Time",
224
- "TIT1" => "Content group description",
225
- "TIT2" => "Title/songname/content description",
226
- "TIT3" => "Subtitle/Description refinement",
227
- "TKEY" => "Initial key",
228
- "TLAN" => "Language(s)",
229
- "TLEN" => "Length",
230
- "TMED" => "Media type",
231
- "TOAL" => "Original album/movie/show title",
232
- "TOFN" => "Original filename",
233
- "TOLY" => "Original lyricist(s)/text writer(s)",
234
- "TOPE" => "Original artist(s)/performer(s)",
235
- "TORY" => "Original release year",
236
- "TOWN" => "File owner/licensee",
237
- "TPE1" => "Lead performer(s)/Soloist(s)",
238
- "TPE2" => "Band/orchestra/accompaniment",
239
- "TPE3" => "Conductor/performer refinement",
240
- "TPE4" => "Interpreted, remixed, or otherwise modified by",
241
- "TPOS" => "Part of a set",
242
- "TPUB" => "Publisher",
243
- "TRCK" => "Track number/Position in set",
244
- "TRDA" => "Recording dates",
245
- "TRSN" => "Internet radio station name",
246
- "TRSO" => "Internet radio station owner",
247
- "TSIZ" => "Size",
248
- "TSRC" => "ISRC (international standard recording code)",
249
- "TSSE" => "Software/Hardware and settings used for encoding",
250
- "TYER" => "Year",
251
- "TXXX" => "User defined text information frame",
252
- "UFID" => "Unique file identifier",
253
- "USER" => "Terms of use",
254
- "USLT" => "Unsychronized lyric/text transcription",
255
- "WCOM" => "Commercial information",
256
- "WCOP" => "Copyright/Legal information",
257
- "WOAF" => "Official audio file webpage",
258
- "WOAR" => "Official artist/performer webpage",
259
- "WOAS" => "Official audio source webpage",
260
- "WORS" => "Official internet radio station homepage",
261
- "WPAY" => "Payment",
262
- "WPUB" => "Publishers official webpage",
263
- "WXXX" => "User defined URL link frame"
264
- }
265
-
266
69
  TAGSIZE = 128
267
70
  #MAX_FRAME_COUNT = 6 #number of frame to read for encoder detection
71
+ V1_V2_TAG_MAPPING = {
72
+ "title" => "TIT2",
73
+ "artist" => "TPE1",
74
+ "album" => "TALB",
75
+ "year" => "TYER",
76
+ "tracknum" => "TRCK",
77
+ "comments" => "COMM",
78
+ "genre_s" => "TCON"
79
+ }
80
+
268
81
 
269
82
  # mpeg version = 1 or 2
270
83
  attr_reader(:mpeg_version)
@@ -291,14 +104,16 @@ class Mp3Info
291
104
  attr_reader(:error_protection)
292
105
 
293
106
  #a sort of "universal" tag, regardless of the tag version, 1 or 2, with the same keys as @tag1
107
+ #this tag has priority over @tag1 and @tag2 when writing the tag with #close
294
108
  attr_reader(:tag)
295
109
 
296
- # id3v1 tag has a Hash. You can modify it, it will be written when calling
110
+ # id3v1 tag as a Hash. You can modify it, it will be written when calling
297
111
  # "close" method.
298
112
  attr_accessor(:tag1)
299
113
 
300
- # id3v2 tag as a Hash
301
- attr_reader(:tag2)
114
+ # id3v2 tag attribute as an ID3v2 object. You can modify it, it will be written when calling
115
+ # "close" method.
116
+ attr_accessor(:tag2)
302
117
 
303
118
  # the original filename
304
119
  attr_reader(:filename)
@@ -326,7 +141,14 @@ class Mp3Info
326
141
  def self.removetag1(filename)
327
142
  if self.hastag1?(filename)
328
143
  newsize = File.size(filename) - TAGSIZE
329
- File.open(filename, "r+") { |f| f.truncate(newsize) }
144
+ File.open(filename, "rb+") { |f| f.truncate(newsize) }
145
+ end
146
+ end
147
+
148
+ # Remove id3v2 tag from +filename+
149
+ def self.removetag2(filename)
150
+ self.open(filename) do |mp3|
151
+ mp3.tag2.clear
330
152
  end
331
153
  end
332
154
 
@@ -335,95 +157,112 @@ class Mp3Info
335
157
  $stderr.puts("#{self.class}::new() does not take block; use #{self.class}::open() instead") if block_given?
336
158
  raise(Mp3InfoError, "empty file") unless File.stat(filename).size? #FIXME
337
159
  @filename = filename
338
- @hastag1, @hastag2 = false
339
- @tag = Hash.new
340
- @tag1 = Hash.new
341
- @tag2 = Hash.new
160
+ @hastag1 = false
161
+
162
+ @tag1 = {}
163
+ @tag1.extend(HashKeys)
164
+
165
+ @tag2 = ID3v2.new
342
166
 
343
167
  @file = File.new(filename, "rb")
344
- parse_tags
345
- @tag_orig = @tag1.dup
346
-
347
- #creation of a sort of "universal" tag, regardless of the tag version
348
- if hastag2?
349
- h = {
350
- "title" => "TIT2",
351
- "artist" => "TPE1",
352
- "album" => "TALB",
353
- "year" => "TYER",
354
- "tracknum" => "TRCK",
355
- "comments" => "COMM",
356
- "genre" => 255,
357
- "genre_s" => "TCON"
358
- }
359
-
360
- h.each { |k, v| @tag[k] = @tag2[v] }
361
-
362
- elsif hastag1?
363
- @tag = @tag1.dup
364
- end
168
+ @file.extend(Mp3FileMethods)
169
+
170
+ begin
171
+ parse_tags
172
+ @tag1_orig = @tag1.dup
365
173
 
174
+ @tag = {}
366
175
 
367
- ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
368
- ### head (fixnum) = valid 4 byte MPEG header
369
-
370
- found = false
176
+ if hastag1?
177
+ @tag = @tag1.dup
178
+ end
371
179
 
372
- 5.times do
373
- head = find_next_frame()
374
- @mpeg_version = [2, 1][head[19]]
375
- @layer = LAYER[head.bits(18,17)]
376
- next if @layer.nil?
377
- @bitrate = BITRATE[@mpeg_version-1][@layer-1][head.bits(15,12)-1]
378
- @error_protection = head[16] == 0 ? true : false
379
- @samplerate = SAMPLERATE[@mpeg_version-1][head.bits(11,10)]
380
- @padding = (head[9] == 1 ? true : false)
381
- @channel_mode = CHANNEL_MODE[@channel_num = head.bits(7,6)]
382
- @copyright = (head[3] == 1 ? true : false)
383
- @original = (head[2] == 1 ? true : false)
384
- @vbr = false
385
- found = true
386
- break
387
- end
180
+ if hastag2?
181
+ @tag = {}
182
+ #creation of a sort of "universal" tag, regardless of the tag version
183
+ V1_V2_TAG_MAPPING.each do |key1, key2|
184
+ t2 = @tag2[key2]
185
+ next unless t2
186
+ @tag[key1] = t2.is_a?(Array) ? t2.first : t2
187
+
188
+ if key1 == "tracknum"
189
+ val = @tag2[key2].is_a?(Array) ? @tag2[key2].first : @tag2[key2]
190
+ @tag[key1] = val.to_i
191
+ end
192
+ end
193
+ end
388
194
 
389
- raise(Mp3InfoError, "Cannot find good frame") unless found
195
+ @tag.extend(HashKeys)
196
+ @tag_orig = @tag.dup
390
197
 
391
198
 
392
- seek = @mpeg_version == 1 ?
393
- (@channel_num == 3 ? 17 : 32) :
394
- (@channel_num == 3 ? 9 : 17)
199
+ ### extracts MPEG info from MPEG header and stores it in the hash @mpeg
200
+ ### head (fixnum) = valid 4 byte MPEG header
201
+
202
+ found = false
395
203
 
396
- @file.seek(seek, IO::SEEK_CUR)
397
-
398
- vbr_head = @file.read(4)
399
- if vbr_head == "Xing"
400
- flags = @file.get32bits
401
- @streamsize = @frames = 0
402
- flags[1] == 1 and @frames = @file.get32bits
403
- flags[2] == 1 and @streamsize = @file.get32bits
404
- # currently this just skips the TOC entries if they're found
405
- @file.seek(100, IO::SEEK_CUR) if flags[0] == 1
406
- @vbr_quality = @file.get32bits if flags[3] == 1
407
- @length = (26/1000.0)*@frames
408
- @bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
409
- @vbr = true
410
- else
411
- # for cbr, calculate duration with the given bitrate
412
- @streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@hastag2 ? @tag2["length"] : 0)
413
- @length = ((@streamsize << 3)/1000.0)/@bitrate
414
- if @tag2["TLEN"]
415
- # but if another duration is given and it isn't close (within 5%)
416
- # assume the mp3 is vbr and go with the given duration
417
- tlen = (@tag2["TLEN"].to_i)/1000
418
- percent_diff = ((@length.to_i-tlen)/tlen.to_f)
419
- if percent_diff.abs > 0.05
420
- # without the xing header, this is the best guess without reading
421
- # every single frame
422
- @vbr = true
423
- @length = @tag2["TLEN"].to_i/1000
424
- @bitrate = (@streamsize / @bitrate) >> 10
425
- end
204
+ 5.times do
205
+ head = find_next_frame()
206
+ head.extend(NumericBits)
207
+ @mpeg_version = [2, 1][head[19]]
208
+ @layer = LAYER[head.bits(18,17)]
209
+ next if @layer.nil?
210
+ @bitrate = BITRATE[@mpeg_version-1][@layer-1][head.bits(15,12)-1]
211
+ @error_protection = head[16] == 0 ? true : false
212
+ @samplerate = SAMPLERATE[@mpeg_version-1][head.bits(11,10)]
213
+ @padding = (head[9] == 1 ? true : false)
214
+ @channel_mode = CHANNEL_MODE[@channel_num = head.bits(7,6)]
215
+ @copyright = (head[3] == 1 ? true : false)
216
+ @original = (head[2] == 1 ? true : false)
217
+ @vbr = false
218
+ found = true
219
+ break
220
+ end
221
+
222
+ raise(Mp3InfoError, "Cannot find good frame") unless found
223
+
224
+
225
+ seek = @mpeg_version == 1 ?
226
+ (@channel_num == 3 ? 17 : 32) :
227
+ (@channel_num == 3 ? 9 : 17)
228
+
229
+ @file.seek(seek, IO::SEEK_CUR)
230
+
231
+ vbr_head = @file.read(4)
232
+ if vbr_head == "Xing"
233
+ puts "Xing header (VBR) detected" if $DEBUG
234
+ flags = @file.get32bits
235
+ @streamsize = @frames = 0
236
+ flags[1] == 1 and @frames = @file.get32bits
237
+ flags[2] == 1 and @streamsize = @file.get32bits
238
+ puts "#{@frames} frames" if $DEBUG
239
+ raise(Mp3InfoError, "bad VBR header") if @frames.zero?
240
+ # currently this just skips the TOC entries if they're found
241
+ @file.seek(100, IO::SEEK_CUR) if flags[0] == 1
242
+ @vbr_quality = @file.get32bits if flags[3] == 1
243
+ @length = (26/1000.0)*@frames
244
+ @bitrate = (((@streamsize/@frames)*@samplerate)/144) >> 10
245
+ @vbr = true
246
+ else
247
+ # for cbr, calculate duration with the given bitrate
248
+ @streamsize = @file.stat.size - (@hastag1 ? TAGSIZE : 0) - (@tag2.valid? ? @tag2.io_position : 0)
249
+ @length = ((@streamsize << 3)/1000.0)/@bitrate
250
+ if @tag2["TLEN"]
251
+ # but if another duration is given and it isn't close (within 5%)
252
+ # assume the mp3 is vbr and go with the given duration
253
+ tlen = (@tag2["TLEN"].is_a?(Array) ? @tag2["TLEN"].last : @tag2["TLEN"]).to_i/1000
254
+ percent_diff = ((@length.to_i-tlen)/tlen.to_f)
255
+ if percent_diff.abs > 0.05
256
+ # without the xing header, this is the best guess without reading
257
+ # every single frame
258
+ @vbr = true
259
+ @length = @tag2["TLEN"].to_i/1000
260
+ @bitrate = (@streamsize / @bitrate) >> 10
261
+ end
262
+ end
426
263
  end
264
+ ensure
265
+ @file.close
427
266
  end
428
267
  end
429
268
 
@@ -452,50 +291,104 @@ class Mp3Info
452
291
  end
453
292
  self
454
293
  end
294
+
295
+ def removetag2
296
+ @tag2.clear
297
+ end
455
298
 
456
- # Has file an id3v1 or v2 tag? true or false
299
+ # Does the file has an id3v1 or v2 tag?
457
300
  def hastag?
458
- @hastag1 or @hastag2
301
+ @hastag1 or @tag2.valid?
459
302
  end
460
303
 
461
- # Has file an id3v1 tag? true or false
304
+ # Does the file has an id3v1 tag?
462
305
  def hastag1?
463
306
  @hastag1
464
307
  end
465
308
 
466
- # Has file an id3v2 tag? true or false
309
+ # Does the file has an id3v2 tag?
467
310
  def hastag2?
468
- @hastag2
311
+ @tag2.valid?
469
312
  end
470
313
 
314
+ # write to another filename at close()
315
+ def rename(new_filename)
316
+ @filename = new_filename
317
+ end
471
318
 
472
319
  # Flush pending modifications to tags and close the file
473
320
  def close
474
- return if @file.nil?
475
- if @tag1 != @tag_orig
476
- @tag_orig.update(@tag1)
477
- #puts "@tag_orig: #{@tag_orig.inspect}"
478
- @file.reopen(@filename, 'rb+')
479
- @file.seek(-TAGSIZE, File::SEEK_END)
480
- t = @file.read(3)
481
- if t != 'TAG'
482
- #append new tag
483
- @file.seek(0, File::SEEK_END)
484
- @file.write('TAG')
321
+ puts "close" if $DEBUG
322
+ if @tag != @tag_orig
323
+ puts "@tag has changed" if $DEBUG
324
+ @tag.each do |k, v|
325
+ @tag1[k] = v
326
+ end
327
+
328
+ V1_V2_TAG_MAPPING.each do |key1, key2|
329
+ @tag2[key2] = @tag[key1] if @tag[key1]
330
+ end
331
+ end
332
+
333
+ if @tag1 != @tag1_orig
334
+ puts "@tag1 has changed" if $DEBUG
335
+ raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
336
+ @tag1_orig.update(@tag1)
337
+ #puts "@tag1_orig: #{@tag1_orig.inspect}"
338
+ File.open(@filename, 'rb+') do |file|
339
+ file.seek(-TAGSIZE, File::SEEK_END)
340
+ t = file.read(3)
341
+ if t != 'TAG'
342
+ #append new tag
343
+ file.seek(0, File::SEEK_END)
344
+ file.write('TAG')
345
+ end
346
+ str = [
347
+ @tag1_orig["title"]||"",
348
+ @tag1_orig["artist"]||"",
349
+ @tag1_orig["album"]||"",
350
+ ((@tag1_orig["year"] != 0) ? ("%04d" % @tag1_orig["year"].to_i) : "\0\0\0\0"),
351
+ @tag1_orig["comments"]||"",
352
+ 0,
353
+ @tag1_orig["tracknum"]||0,
354
+ @tag1_orig["genre"]||255
355
+ ].pack("Z30Z30Z30Z4Z28CCC")
356
+ file.write(str)
357
+ end
358
+ end
359
+
360
+ if @tag2.changed?
361
+ puts "@tag2 has changed" if $DEBUG
362
+ raise(Mp3InfoError, "file is not writable") unless File.writable?(@filename)
363
+ tempfile_name = nil
364
+ File.open(@filename, 'rb+') do |file|
365
+
366
+ #if tag2 already exists, seek to end of it
367
+ if @tag2.valid?
368
+ file.seek(@tag2.io_position)
369
+ end
370
+ # if @file.read(3) == "ID3"
371
+ # version_maj, version_min, flags = @file.read(3).unpack("CCB4")
372
+ # unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
373
+ # tag2_len = @file.get_syncsafe
374
+ # @file.seek(@file.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
375
+ # @file.seek(tag2_len, IO::SEEK_CUR)
376
+ # end
377
+ tempfile_name = @filename + ".tmp"
378
+ File.open(tempfile_name, "wb") do |tempfile|
379
+ unless @tag2.empty?
380
+ tempfile.write("ID3")
381
+ tempfile.write(@tag2.to_bin)
382
+ end
383
+
384
+ bufsiz = file.stat.blksize || 4096
385
+ while buf = file.read(bufsiz)
386
+ tempfile.write(buf)
387
+ end
388
+ end
485
389
  end
486
- str = [
487
- @tag_orig["title"]||"",
488
- @tag_orig["artist"]||"",
489
- @tag_orig["album"]||"",
490
- ((@tag_orig["year"] != 0) ? ("%04d" % @tag_orig["year"]) : "\0\0\0\0"),
491
- @tag_orig["comments"]||"",
492
- 0,
493
- @tag_orig["tracknum"]||0,
494
- @tag_orig["genre"]||255
495
- ].pack("Z30Z30Z30Z4Z28CCC")
496
- @file.write(str)
390
+ File.rename(tempfile_name, @filename)
497
391
  end
498
- @file.close
499
392
  @file = nil
500
393
  end
501
394
 
@@ -503,24 +396,25 @@ class Mp3Info
503
396
  def to_s
504
397
  s = "MPEG #{@mpeg_version} Layer #{@layer} #{@vbr ? "VBR" : "CBR"} #{@bitrate} Kbps #{@channel_mode} #{@samplerate} Hz length #{@length} sec. error protection #{@error_protection} "
505
398
  s << "tag1: "+@tag1.inspect+"\n" if @hastag1
506
- s << "tag2: "+@tag2.inspect+"\n" if @hastag2
399
+ s << "tag2: "+@tag2.inspect+"\n" if @tag2.valid?
507
400
  s
508
401
  end
509
402
 
510
403
 
511
404
  private
512
-
405
+
513
406
  ### parses the id3 tags of the currently open @file
514
407
  def parse_tags
515
408
  return if @file.stat.size < TAGSIZE # file is too small
516
409
  @file.seek(0)
517
410
  f3 = @file.read(3)
518
411
  gettag1 if f3 == "TAG" # v1 tag at beginning
519
- gettag2 if f3 == "ID3" # v2 tag at beginning
412
+ @tag2.from_io(@file) if f3 == "ID3" # v2 tag at beginning
413
+
520
414
  unless @hastag1 # v1 tag at end
521
415
  # this preserves the file pos if tag2 found, since gettag2 leaves
522
416
  # the file at the best guess as to the first MPEG frame
523
- pos = (@hastag2 ? @file.pos : 0)
417
+ pos = (@tag2.valid? ? @file.pos : 0)
524
418
  # seek to where id3v1 tag should be
525
419
  @file.seek(-TAGSIZE, IO::SEEK_END)
526
420
  gettag1 if @file.read(3) == "TAG"
@@ -564,109 +458,6 @@ private
564
458
  @tag1["genre_s"] = GENRES[@tag1["genre"]] || ""
565
459
  end
566
460
 
567
- ### gets id3v2 tag information from @file
568
- def gettag2
569
- @file.seek(3)
570
- version_maj, version_min, flags = @file.read(3).unpack("CCB4")
571
- unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
572
- return unless [2, 3, 4].include?(version_maj)
573
- @hastag2 = true
574
- @tag2["version"] = "2.#{version_maj}.#{version_min}"
575
- tag2_len = @file.get_syncsafe
576
- case version_maj
577
- when 2
578
- read_id3v2_2_frames(tag2_len)
579
- when 3,4
580
- # seek past extended header if present
581
- @file.seek(@file.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
582
- read_id3v2_3_frames(tag2_len)
583
- end
584
- tag2["length"] = @file.pos
585
- # we should now have @file sitting at the first MPEG frame
586
- end
587
-
588
- ### runs thru @file one char at a time looking for best guess of first MPEG
589
- ### frame, which should be first 0xff byte after id3v2 padding zero's
590
- ### returns true
591
- def v2_end?
592
- until @file.getc == 0xff
593
- end
594
- @file.seek(-1, IO::SEEK_CUR)
595
- true
596
- end
597
-
598
- ### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
599
- ### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
600
- ### NOTE: the id3v2 header does not take padding zero's into consideration
601
- def read_id3v2_3_frames(tag2_len)
602
- v2end_found = false
603
- until v2end_found # there are 2 ways to end the loop
604
- name = @file.read(4)
605
- if name[0] == 0
606
- @file.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
607
- v2end_found = v2_end? # so we seek to end of zeros
608
- else
609
- size = @file.get32bits
610
- @file.seek(2, IO::SEEK_CUR) # skip flags
611
- add_value_to_tag2(name, size)
612
- # case name
613
- # when /T[A-Z]+|COMM/
614
- # data = read_id3_string(size-1)
615
- # add_value_to_tag2(name, data)
616
- # else
617
- # @file.seek(size-1, IO::SEEK_CUR)
618
- # end
619
- v2end_found = true if @file.pos >= tag2_len # 2. reach length from header
620
- end
621
- end
622
- end
623
-
624
- ### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
625
- ### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
626
- ### NOTE: the id3v2 header does not take padding zero's into consideration
627
- def read_id3v2_2_frames(tag2_len)
628
- v2end_found = false
629
- until v2end_found
630
- name = @file.read(3)
631
- if name[0] == 0
632
- @file.seek(-3, IO::SEEK_CUR)
633
- v2end_found = v2_end?
634
- else
635
- size = (@file.getc << 16) + (@file.getc << 8) + @file.getc
636
- add_value_to_tag2(name, size)
637
- v2end_found = true if @file.pos >= tag2_len
638
- end
639
- end
640
- end
641
-
642
- ### Add data to tag2["name"]
643
- ### read lang_encoding, decode data if unicode and
644
- ### create an array if the key ever exists in the tag
645
- def add_value_to_tag2(name, size)
646
- lang_encoding = @file.getc # language encoding bit 0 for iso_8859_1, 1 for unicode
647
- data = size == 0 ? "" : @file.read(size-1)
648
-
649
- if lang_encoding == 1 and name[0] == ?T
650
- require "iconv"
651
-
652
- #strip byte-order bytes at the beginning of the unicode string if they exists
653
- data[0..3] =~ /^[\xff\xfe]+$/ and data = data[2..-1]
654
-
655
- data = Iconv.iconv("ISO-8859-1", "UNICODE", data)[0]
656
- end
657
-
658
- if @tag2.keys.include?(name)
659
- unless @tag2[name].is_a?(Array)
660
- keep = @tag2[name]
661
- @tag2[name] = []
662
- @tag2[name] << keep
663
- end
664
- @tag2[name] << data
665
- else
666
- @tag2[name] = data
667
- end
668
- end
669
-
670
461
  ### reads through @file from current pos until it finds a valid MPEG header
671
462
  ### returns the MPEG header as FixNum
672
463
  def find_next_frame
@@ -674,11 +465,14 @@ private
674
465
  # It should be at byte 0 when there's no id3v2 tag.
675
466
  # It should be at the end of the id3v2 tag or the zero padding if there
676
467
  # is a id3v2 tag.
677
- start_pos = @file.pos
678
- dummyproof = @file.stat.size - @file.pos
468
+
469
+ #dummyproof = @file.stat.size - @file.pos => WAS TOO MUCH
470
+ dummyproof = [ @file.stat.size - @file.pos, 2000000 ].min
679
471
  dummyproof.times do |i|
680
472
  if @file.getc == 0xff
681
- head = 0xff000000 + (@file.getc << 16) + (@file.getc << 8) + @file.getc
473
+ data = @file.read(3)
474
+ raise Mp3InfoError if @file.eof?
475
+ head = 0xff000000 + (data[0] << 16) + (data[1] << 8) + data[2]
682
476
  if check_head(head)
683
477
  return head
684
478
  else
@@ -686,7 +480,7 @@ private
686
480
  end
687
481
  end
688
482
  end
689
- raise Mp3InfoError
483
+ raise Mp3InfoError, "cannot find a valid frame after reading #{dummyproof} bytes"
690
484
  end
691
485
 
692
486
  ### checks the given header to see if it is valid