ruby-mp3info 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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