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 +61 -0
- data/EXAMPLES +40 -0
- data/README +28 -0
- data/lib/mp3info.rb +221 -427
- data/lib/mp3info/extension_modules.rb +36 -0
- data/lib/mp3info/id3v2.rb +311 -0
- data/test.rb +325 -0
- metadata +27 -13
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.
|
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
|
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
|
301
|
-
|
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, "
|
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
|
339
|
-
|
340
|
-
@tag1 =
|
341
|
-
@
|
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
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
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
|
-
|
368
|
-
|
369
|
-
|
370
|
-
found = false
|
176
|
+
if hastag1?
|
177
|
+
@tag = @tag1.dup
|
178
|
+
end
|
371
179
|
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
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
|
-
|
195
|
+
@tag.extend(HashKeys)
|
196
|
+
@tag_orig = @tag.dup
|
390
197
|
|
391
198
|
|
392
|
-
|
393
|
-
(
|
394
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
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
|
-
#
|
299
|
+
# Does the file has an id3v1 or v2 tag?
|
457
300
|
def hastag?
|
458
|
-
@hastag1 or @
|
301
|
+
@hastag1 or @tag2.valid?
|
459
302
|
end
|
460
303
|
|
461
|
-
#
|
304
|
+
# Does the file has an id3v1 tag?
|
462
305
|
def hastag1?
|
463
306
|
@hastag1
|
464
307
|
end
|
465
308
|
|
466
|
-
#
|
309
|
+
# Does the file has an id3v2 tag?
|
467
310
|
def hastag2?
|
468
|
-
@
|
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
|
-
|
475
|
-
if @
|
476
|
-
@
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
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
|
-
|
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 @
|
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
|
-
|
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 = (@
|
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
|
-
|
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
|
-
|
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
|