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
@@ -0,0 +1,36 @@
|
|
1
|
+
class Mp3Info
|
2
|
+
module HashKeys #:nodoc:
|
3
|
+
### lets you specify hash["key"] as hash.key
|
4
|
+
### this came from CodingInRuby on RubyGarden
|
5
|
+
### http://www.rubygarden.org/ruby?CodingInRuby
|
6
|
+
def method_missing(meth,*args)
|
7
|
+
m = meth.id2name
|
8
|
+
if /=$/ =~ m
|
9
|
+
self[m.chop] = (args.length<2 ? args[0] : args)
|
10
|
+
else
|
11
|
+
self[m]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module NumericBits #:nodoc:
|
17
|
+
### returns the selected bit range (b, a) as a number
|
18
|
+
### NOTE: b > a if not, returns 0
|
19
|
+
def bits(b, a)
|
20
|
+
t = 0
|
21
|
+
b.downto(a) { |i| t += t + self[i] }
|
22
|
+
t
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Mp3FileMethods #:nodoc:
|
27
|
+
def get32bits
|
28
|
+
(getc << 24) + (getc << 16) + (getc << 8) + getc
|
29
|
+
end
|
30
|
+
def get_syncsafe
|
31
|
+
(getc << 21) + (getc << 14) + (getc << 7) + getc
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,311 @@
|
|
1
|
+
require "mp3info/extension_modules"
|
2
|
+
|
3
|
+
# This class is not intended to be used directly
|
4
|
+
class ID3v2 < DelegateClass(Hash)
|
5
|
+
VERSION_MAJ = 3
|
6
|
+
|
7
|
+
TAGS = {
|
8
|
+
"AENC" => "Audio encryption",
|
9
|
+
"APIC" => "Attached picture",
|
10
|
+
"COMM" => "Comments",
|
11
|
+
"COMR" => "Commercial frame",
|
12
|
+
"ENCR" => "Encryption method registration",
|
13
|
+
"EQUA" => "Equalization",
|
14
|
+
"ETCO" => "Event timing codes",
|
15
|
+
"GEOB" => "General encapsulated object",
|
16
|
+
"GRID" => "Group identification registration",
|
17
|
+
"IPLS" => "Involved people list",
|
18
|
+
"LINK" => "Linked information",
|
19
|
+
"MCDI" => "Music CD identifier",
|
20
|
+
"MLLT" => "MPEG location lookup table",
|
21
|
+
"OWNE" => "Ownership frame",
|
22
|
+
"PRIV" => "Private frame",
|
23
|
+
"PCNT" => "Play counter",
|
24
|
+
"POPM" => "Popularimeter",
|
25
|
+
"POSS" => "Position synchronisation frame",
|
26
|
+
"RBUF" => "Recommended buffer size",
|
27
|
+
"RVAD" => "Relative volume adjustment",
|
28
|
+
"RVRB" => "Reverb",
|
29
|
+
"SYLT" => "Synchronized lyric/text",
|
30
|
+
"SYTC" => "Synchronized tempo codes",
|
31
|
+
"TALB" => "Album/Movie/Show title",
|
32
|
+
"TBPM" => "BPM (beats per minute)",
|
33
|
+
"TCOM" => "Composer",
|
34
|
+
"TCON" => "Content type",
|
35
|
+
"TCOP" => "Copyright message",
|
36
|
+
"TDAT" => "Date",
|
37
|
+
"TDLY" => "Playlist delay",
|
38
|
+
"TENC" => "Encoded by",
|
39
|
+
"TEXT" => "Lyricist/Text writer",
|
40
|
+
"TFLT" => "File type",
|
41
|
+
"TIME" => "Time",
|
42
|
+
"TIT1" => "Content group description",
|
43
|
+
"TIT2" => "Title/songname/content description",
|
44
|
+
"TIT3" => "Subtitle/Description refinement",
|
45
|
+
"TKEY" => "Initial key",
|
46
|
+
"TLAN" => "Language(s)",
|
47
|
+
"TLEN" => "Length",
|
48
|
+
"TMED" => "Media type",
|
49
|
+
"TOAL" => "Original album/movie/show title",
|
50
|
+
"TOFN" => "Original filename",
|
51
|
+
"TOLY" => "Original lyricist(s)/text writer(s)",
|
52
|
+
"TOPE" => "Original artist(s)/performer(s)",
|
53
|
+
"TORY" => "Original release year",
|
54
|
+
"TOWN" => "File owner/licensee",
|
55
|
+
"TPE1" => "Lead performer(s)/Soloist(s)",
|
56
|
+
"TPE2" => "Band/orchestra/accompaniment",
|
57
|
+
"TPE3" => "Conductor/performer refinement",
|
58
|
+
"TPE4" => "Interpreted, remixed, or otherwise modified by",
|
59
|
+
"TPOS" => "Part of a set",
|
60
|
+
"TPUB" => "Publisher",
|
61
|
+
"TRCK" => "Track number/Position in set",
|
62
|
+
"TRDA" => "Recording dates",
|
63
|
+
"TRSN" => "Internet radio station name",
|
64
|
+
"TRSO" => "Internet radio station owner",
|
65
|
+
"TSIZ" => "Size",
|
66
|
+
"TSRC" => "ISRC (international standard recording code)",
|
67
|
+
"TSSE" => "Software/Hardware and settings used for encoding",
|
68
|
+
"TYER" => "Year",
|
69
|
+
"TXXX" => "User defined text information frame",
|
70
|
+
"UFID" => "Unique file identifier",
|
71
|
+
"USER" => "Terms of use",
|
72
|
+
"USLT" => "Unsychronized lyric/text transcription",
|
73
|
+
"WCOM" => "Commercial information",
|
74
|
+
"WCOP" => "Copyright/Legal information",
|
75
|
+
"WOAF" => "Official audio file webpage",
|
76
|
+
"WOAR" => "Official artist/performer webpage",
|
77
|
+
"WOAS" => "Official audio source webpage",
|
78
|
+
"WORS" => "Official internet radio station homepage",
|
79
|
+
"WPAY" => "Payment",
|
80
|
+
"WPUB" => "Publishers official webpage",
|
81
|
+
"WXXX" => "User defined URL link frame"
|
82
|
+
}
|
83
|
+
|
84
|
+
include Mp3Info::HashKeys
|
85
|
+
|
86
|
+
attr_reader :io_position
|
87
|
+
|
88
|
+
# possibles keys:
|
89
|
+
# :+lang+ for writing comments
|
90
|
+
# :+encoding+: :+iso+ or :+unicode+
|
91
|
+
attr_reader :options
|
92
|
+
|
93
|
+
def initialize(options = {})
|
94
|
+
@options = {
|
95
|
+
:lang => "ENG",
|
96
|
+
:encoding => :iso #language encoding bit 0 for iso_8859_1, 1 for unicode
|
97
|
+
}
|
98
|
+
@options.update(options)
|
99
|
+
|
100
|
+
@hash = {}
|
101
|
+
#TAGS.keys.each { |k| @hash[k] = nil }
|
102
|
+
@hash_orig = {}
|
103
|
+
super(@hash)
|
104
|
+
@valid = false
|
105
|
+
@version_maj = @version_min = nil
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid?
|
109
|
+
@valid
|
110
|
+
end
|
111
|
+
|
112
|
+
def changed?
|
113
|
+
@hash_orig != @hash
|
114
|
+
end
|
115
|
+
|
116
|
+
def version
|
117
|
+
"2.#{@version_maj}.#{@version_min}"
|
118
|
+
end
|
119
|
+
|
120
|
+
### gets id3v2 tag information from io
|
121
|
+
def from_io(io)
|
122
|
+
@io = io
|
123
|
+
version_maj, version_min, flags = @io.read(3).unpack("CCB4")
|
124
|
+
unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
|
125
|
+
raise("can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
|
126
|
+
@version_maj, @version_min = version_maj, version_min
|
127
|
+
@valid = true
|
128
|
+
tag2_len = @io.get_syncsafe
|
129
|
+
case @version_maj
|
130
|
+
when 2
|
131
|
+
read_id3v2_2_frames(tag2_len)
|
132
|
+
when 3,4
|
133
|
+
# seek past extended header if present
|
134
|
+
@io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
|
135
|
+
read_id3v2_3_frames(tag2_len)
|
136
|
+
end
|
137
|
+
@io_position = @io.pos
|
138
|
+
|
139
|
+
@hash_orig = @hash.dup
|
140
|
+
#no more reading
|
141
|
+
@io = nil
|
142
|
+
# we should now have io sitting at the first MPEG frame
|
143
|
+
end
|
144
|
+
|
145
|
+
def to_bin
|
146
|
+
#TODO handle of @tag2[TLEN"]
|
147
|
+
#TODO add of crc
|
148
|
+
#TODO add restrictions tag
|
149
|
+
|
150
|
+
tag = ""
|
151
|
+
@hash.each do |k, v|
|
152
|
+
next unless v
|
153
|
+
next if v.respond_to?("empty?") and v.empty?
|
154
|
+
data = encode_tag(k, v.to_s)
|
155
|
+
#data << "\x00"*2 #End of tag
|
156
|
+
|
157
|
+
tag << k[0,4] #4 characte max for a tag's key
|
158
|
+
#tag << to_syncsafe(data.size) #+1 because of the language encoding byte
|
159
|
+
tag << [data.size].pack("N") #+1 because of the language encoding byte
|
160
|
+
tag << "\x00"*2 #flags
|
161
|
+
tag << data
|
162
|
+
end
|
163
|
+
|
164
|
+
tag_str = ""
|
165
|
+
#version_maj, version_min, unsync, ext_header, experimental, footer
|
166
|
+
tag_str << [ VERSION_MAJ, 0, "0000" ].pack("CCB4")
|
167
|
+
tag_str << to_syncsafe(tag.size)
|
168
|
+
tag_str << tag
|
169
|
+
p tag_str if $DEBUG
|
170
|
+
tag_str
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
|
176
|
+
def encode_tag(name, value)
|
177
|
+
puts "encode_tag(#{name.inspect}, #{value.inspect})" if $DEBUG
|
178
|
+
case name
|
179
|
+
when "COMM"
|
180
|
+
[ @options[:encoding] == :iso ? 0 : 1, @options[:lang], 0, value ].pack("ca3ca*")
|
181
|
+
else
|
182
|
+
if @options[:encoding] == :iso
|
183
|
+
"\x00"+value
|
184
|
+
else
|
185
|
+
"\x01"+value #Iconv.iconv("UNICODE", "ISO-8859-1", value)[0]
|
186
|
+
end
|
187
|
+
#data << "\x00"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
### Read a tag from file and perform UNICODE translation if needed
|
192
|
+
def decode_tag(name, value)
|
193
|
+
case name
|
194
|
+
when "COMM"
|
195
|
+
#FIXME improve this
|
196
|
+
encoding, lang, str = value.unpack("ca3a*")
|
197
|
+
out = value.split(0.chr).last
|
198
|
+
else
|
199
|
+
encoding = value[0] # language encoding bit 0 for iso_8859_1, 1 for unicode
|
200
|
+
out = value[1..-1]
|
201
|
+
end
|
202
|
+
|
203
|
+
if encoding == 1 #and name[0] == ?T
|
204
|
+
require "iconv"
|
205
|
+
|
206
|
+
#strip byte-order bytes at the beginning of the unicode string if they exists
|
207
|
+
out[0..3] =~ /^[\xff\xfe]+$/ and out = out[2..-1]
|
208
|
+
begin
|
209
|
+
out = Iconv.iconv("ISO-8859-1", "UNICODE", out)[0]
|
210
|
+
rescue Iconv::IllegalSequence, Iconv::InvalidCharacter
|
211
|
+
end
|
212
|
+
end
|
213
|
+
out
|
214
|
+
end
|
215
|
+
|
216
|
+
### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
|
217
|
+
### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
|
218
|
+
### NOTE: the id3v2 header does not take padding zero's into consideration
|
219
|
+
def read_id3v2_3_frames(tag2_len)
|
220
|
+
loop do # there are 2 ways to end the loop
|
221
|
+
name = @io.read(4)
|
222
|
+
if name[0] == 0 or name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ )
|
223
|
+
@io.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
|
224
|
+
seek_to_v2_end
|
225
|
+
break
|
226
|
+
else
|
227
|
+
#size = @file.get_syncsafe #this seems to be a bug
|
228
|
+
size = @io.get32bits
|
229
|
+
puts "name '#{name}' size #{size} " if $DEBUG
|
230
|
+
@io.seek(2, IO::SEEK_CUR) # skip flags
|
231
|
+
add_value_to_tag2(name, size)
|
232
|
+
# case name
|
233
|
+
# when /^T/
|
234
|
+
# puts "tag is text. reading" if $DEBUG
|
235
|
+
## data = read_id3_string(size-1)
|
236
|
+
## add_value_to_tag2(name, data)
|
237
|
+
# else
|
238
|
+
# decode_tag(
|
239
|
+
# #@file.seek(size-1, IO::SEEK_CUR)
|
240
|
+
# puts "tag is binary, skipping" if $DEBUG
|
241
|
+
# @io.seek(size, IO::SEEK_CUR)
|
242
|
+
# end
|
243
|
+
|
244
|
+
# case name
|
245
|
+
# #FIXME DRY
|
246
|
+
# when "COMM"
|
247
|
+
# data = read_id3v2_frame(size)
|
248
|
+
# lang = data[0,3]
|
249
|
+
# data = data[3,-1]
|
250
|
+
# else
|
251
|
+
# end
|
252
|
+
end
|
253
|
+
break if @io.pos >= tag2_len # 2. reach length from header
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
|
258
|
+
### tag2_len (fixnum) = length of entire id3v2 data, as reported in header
|
259
|
+
### NOTE: the id3v2 header does not take padding zero's into consideration
|
260
|
+
def read_id3v2_2_frames(tag2_len)
|
261
|
+
loop do
|
262
|
+
name = @io.read(3)
|
263
|
+
if name[0] == 0
|
264
|
+
@io.seek(-3, IO::SEEK_CUR)
|
265
|
+
seek_to_v2_end
|
266
|
+
break
|
267
|
+
else
|
268
|
+
size = (@io.getc << 16) + (@io.getc << 8) + @io.getc
|
269
|
+
add_value_to_tag2(name, size)
|
270
|
+
break if @io.pos >= tag2_len
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
### Add data to tag2["name"]
|
276
|
+
### read lang_encoding, decode data if unicode and
|
277
|
+
### create an array if the key already exists in the tag
|
278
|
+
def add_value_to_tag2(name, size)
|
279
|
+
puts "add_value_to_tag2" if $DEBUG
|
280
|
+
data = decode_tag(name, @io.read(size))
|
281
|
+
if self.keys.include?(name)
|
282
|
+
unless self[name].is_a?(Array)
|
283
|
+
self[name] = self[name].to_a
|
284
|
+
end
|
285
|
+
self[name] << data
|
286
|
+
else
|
287
|
+
self[name] = data
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
### runs thru @file one char at a time looking for best guess of first MPEG
|
292
|
+
### frame, which should be first 0xff byte after id3v2 padding zero's
|
293
|
+
def seek_to_v2_end
|
294
|
+
until @io.getc == 0xff
|
295
|
+
end
|
296
|
+
@io.seek(-1, IO::SEEK_CUR)
|
297
|
+
end
|
298
|
+
|
299
|
+
### convert an 32 integer to a syncsafe string
|
300
|
+
def to_syncsafe(num)
|
301
|
+
n = ( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
|
302
|
+
[n].pack("N")
|
303
|
+
end
|
304
|
+
|
305
|
+
# def method_missing(meth, *args)
|
306
|
+
# m = meth.id2name
|
307
|
+
# return nil if TAGS.has_key?(m) and self[m].nil?
|
308
|
+
# super
|
309
|
+
# end
|
310
|
+
end
|
311
|
+
|
data/test.rb
ADDED
@@ -0,0 +1,325 @@
|
|
1
|
+
#!/usr/bin/ruby -w
|
2
|
+
|
3
|
+
$:.unshift("lib/")
|
4
|
+
|
5
|
+
require "test/unit"
|
6
|
+
require "base64"
|
7
|
+
require "mp3info"
|
8
|
+
require "fileutils"
|
9
|
+
|
10
|
+
class Mp3InfoTest < Test::Unit::TestCase
|
11
|
+
|
12
|
+
TEMP_FILE = File.join(File.dirname($0), "test_mp3info.mp3")
|
13
|
+
BASIC_TAG2 = {
|
14
|
+
"COMM" => "comments",
|
15
|
+
#"TCON" => "genre_s"
|
16
|
+
"TIT2" => "title",
|
17
|
+
"TPE1" => "artist",
|
18
|
+
"TALB" => "album",
|
19
|
+
"TYER" => "year",
|
20
|
+
"TRCK" => "tracknum"
|
21
|
+
}
|
22
|
+
|
23
|
+
# aliasing to allow testing with old versions of Test::Unit
|
24
|
+
alias set_up setup
|
25
|
+
alias tear_down teardown
|
26
|
+
|
27
|
+
def setup
|
28
|
+
# Command to create a dummy MP3
|
29
|
+
# dd if=/dev/zero bs=1024 count=15 | lame --preset cbr 128 -r -s 44.1 --bitwidth 16 - - | ruby -rbase64 -e 'print Base64.encode64($stdin.read)'
|
30
|
+
@valid_mp3 = Base64.decode64 <<EOF
|
31
|
+
//uQZAAAAAAAaQYAAAAAAA0gwAAAAAABpBwAAAAAADSDgAAATEFNRTMuOTNV
|
32
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
33
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
34
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
35
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
36
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
37
|
+
VVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuOTNVVVVVVVVVVVVVVVVVVVVVVVVV
|
38
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
39
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
40
|
+
VVVVVVVVVVVVVVVV//uSZL6P8AAAaQAAAAAAAA0gAAAAAAABpAAAAAAAADSA
|
41
|
+
AAAAVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
42
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
43
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
44
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
45
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
46
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjkzVVVVVVVV
|
47
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
48
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
49
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kmT/j/AAAGkAAAAAAAANIAAA
|
50
|
+
AAAAAaQAAAAAAAA0gAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
51
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
52
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
53
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
54
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
55
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVM
|
56
|
+
QU1FMy45M1VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
57
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
58
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5Jk/4/w
|
59
|
+
AABpAAAAAAAADSAAAAAAAAGkAAAAAAAANIAAAABVVVVVVVVVVVVVVVVVVVVV
|
60
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
61
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
62
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
63
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
64
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
65
|
+
VVVVVVVVVVVVVVVVTEFNRTMuOTNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
66
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
67
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
68
|
+
VVVVVVVV//uSZP+P8AAAaQAAAAAAAA0gAAAAAAABpAAAAAAAADSAAAAAVVVV
|
69
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
70
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
71
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
72
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
73
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
74
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
75
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
76
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV
|
77
|
+
VVVVVVVVVVVVVVVVVVVVVVVVVQ==
|
78
|
+
EOF
|
79
|
+
@tag = {
|
80
|
+
"title" => "title",
|
81
|
+
"artist" => "artist",
|
82
|
+
"album" => "album",
|
83
|
+
"year" => 1921,
|
84
|
+
"comments" => "comments",
|
85
|
+
"genre" => 0,
|
86
|
+
"genre_s" => "Blues",
|
87
|
+
"tracknum" => 36
|
88
|
+
}
|
89
|
+
File.open(TEMP_FILE, "w") { |f| f.write(@valid_mp3) }
|
90
|
+
end
|
91
|
+
|
92
|
+
def teardown
|
93
|
+
FileUtils.rm_f(TEMP_FILE)
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
def test_to_s
|
98
|
+
Mp3Info.open(TEMP_FILE) { |info| assert(info.to_s.is_a?(String)) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_not_an_mp3
|
102
|
+
File.open(TEMP_FILE, "w") do |f|
|
103
|
+
str = "0"*1024*1024
|
104
|
+
f.write(str)
|
105
|
+
end
|
106
|
+
assert_raises(Mp3InfoError) {
|
107
|
+
mp3 = Mp3Info.new(TEMP_FILE)
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_is_an_mp3
|
112
|
+
assert_nothing_raised {
|
113
|
+
Mp3Info.new(TEMP_FILE).close
|
114
|
+
}
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_detected_info
|
118
|
+
Mp3Info.open(TEMP_FILE) do |info|
|
119
|
+
assert_equal(info.mpeg_version, 1)
|
120
|
+
assert_equal(info.layer, 3)
|
121
|
+
assert_equal(info.vbr, false)
|
122
|
+
assert_equal(info.bitrate, 128)
|
123
|
+
assert_equal(info.channel_mode, "JStereo")
|
124
|
+
assert_equal(info.samplerate, 44100)
|
125
|
+
assert_equal(info.error_protection, false)
|
126
|
+
assert_equal(info.length, 0.1305625)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_removetag1
|
131
|
+
Mp3Info.open(TEMP_FILE) { |info| info.tag1 = @tag }
|
132
|
+
assert(Mp3Info.hastag1?(TEMP_FILE))
|
133
|
+
Mp3Info.removetag1(TEMP_FILE)
|
134
|
+
assert(! Mp3Info.hastag1?(TEMP_FILE))
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_writetag1
|
138
|
+
Mp3Info.open(TEMP_FILE) { |info| info.tag1 = @tag }
|
139
|
+
Mp3Info.open(TEMP_FILE) { |info| assert(info.tag1 == @tag) }
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_valid_tag1_1
|
143
|
+
tag = [ "title", "artist", "album", "1921", "comments", 36, 0].pack('A30A30A30A4a29CC')
|
144
|
+
valid_tag = {
|
145
|
+
"title" => "title",
|
146
|
+
"artist" => "artist",
|
147
|
+
"album" => "album",
|
148
|
+
"year" => 1921,
|
149
|
+
"comments" => "comments",
|
150
|
+
"genre" => "Blues",
|
151
|
+
#"version" => "1",
|
152
|
+
"tracknum" => 36
|
153
|
+
}
|
154
|
+
id3_test(tag, valid_tag)
|
155
|
+
end
|
156
|
+
|
157
|
+
def test_valid_tag1_0
|
158
|
+
tag = [ "title", "artist", "album", "1921", "comments", 0].pack('A30A30A30A4A30C')
|
159
|
+
valid_tag = {
|
160
|
+
"title" => "title",
|
161
|
+
"artist" => "artist",
|
162
|
+
"album" => "album",
|
163
|
+
"year" => 1921,
|
164
|
+
"comments" => "comments",
|
165
|
+
"genre" => "Blues",
|
166
|
+
#"version" => "0"
|
167
|
+
}
|
168
|
+
id3_test(tag, valid_tag)
|
169
|
+
end
|
170
|
+
|
171
|
+
def id3_test(tag_str, valid_tag)
|
172
|
+
tag = "TAG" + tag_str
|
173
|
+
File.open(TEMP_FILE, "w") do |f|
|
174
|
+
f.write(@valid_mp3)
|
175
|
+
f.write(tag)
|
176
|
+
end
|
177
|
+
assert(Mp3Info.hastag1?(TEMP_FILE))
|
178
|
+
#info = Mp3Info.new(TEMP_FILE)
|
179
|
+
#FIXME validate this test
|
180
|
+
#assert_equal(info.tag1, valid_tag)
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_removetag2
|
184
|
+
w = write_temp_file({"TIT2" => "sdfqdsf"})
|
185
|
+
|
186
|
+
assert( Mp3Info.hastag2?(TEMP_FILE) )
|
187
|
+
Mp3Info.removetag2(TEMP_FILE)
|
188
|
+
assert( ! Mp3Info.hastag2?(TEMP_FILE) )
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_universal_tag
|
192
|
+
2.times do
|
193
|
+
tag = {"title" => "title"}
|
194
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
195
|
+
tag.each { |k,v| mp3.tag[k] = v }
|
196
|
+
end
|
197
|
+
w = Mp3Info.open(TEMP_FILE) { |m| m.tag }
|
198
|
+
assert_equal(tag, w)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def test_id3v2_universal_tag
|
203
|
+
tag = {}
|
204
|
+
%w{comments title artist album}.each { |k| tag[k] = k }
|
205
|
+
tag["tracknum"] = 34
|
206
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
207
|
+
tag.each { |k,v| mp3.tag[k] = v }
|
208
|
+
end
|
209
|
+
w = Mp3Info.open(TEMP_FILE) { |m| m.tag }
|
210
|
+
w.delete("genre")
|
211
|
+
w.delete("genre_s")
|
212
|
+
assert_equal(tag, w)
|
213
|
+
# id3v2_prog_test(tag, w)
|
214
|
+
end
|
215
|
+
|
216
|
+
def test_id3v2_version
|
217
|
+
written_tag = write_temp_file(BASIC_TAG2)
|
218
|
+
assert_equal( "2.3.0", written_tag.version )
|
219
|
+
end
|
220
|
+
|
221
|
+
def test_id3v2_methods
|
222
|
+
tag = { "TIT2" => "tit2", "TPE1" => "tpe1" }
|
223
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
224
|
+
tag.each do |k, v|
|
225
|
+
mp3.tag2.send("#{k}=".to_sym, v)
|
226
|
+
end
|
227
|
+
assert_equal(tag, mp3.tag2)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def test_id3v2_basic
|
232
|
+
w = write_temp_file(BASIC_TAG2)
|
233
|
+
assert_equal(BASIC_TAG2, w)
|
234
|
+
id3v2_prog_test(BASIC_TAG2, w)
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_id3v2_trash
|
238
|
+
end
|
239
|
+
|
240
|
+
def test_id3v2_complex
|
241
|
+
tag = {}
|
242
|
+
#ID3v2::TAGS.keys.each do |k|
|
243
|
+
["PRIV", "APIC"].each do |k|
|
244
|
+
tag[k] = random_string(50)
|
245
|
+
end
|
246
|
+
assert_equal(tag, write_temp_file(tag))
|
247
|
+
end
|
248
|
+
|
249
|
+
def test_id3v2_bigtag
|
250
|
+
tag = {"APIC" => random_string(1024) }
|
251
|
+
assert_equal(tag, write_temp_file(tag))
|
252
|
+
end
|
253
|
+
|
254
|
+
#test the tag with php getid3
|
255
|
+
# prog = %{
|
256
|
+
# <?php
|
257
|
+
# require("/var/www/root/netjuke/lib/getid3/getid3.php");
|
258
|
+
# $mp3info = GetAllFileInfo('#{TEMP_FILE}');
|
259
|
+
# echo $mp3info;
|
260
|
+
# ?>
|
261
|
+
# }
|
262
|
+
#
|
263
|
+
# open("|php", "r+") do |io|
|
264
|
+
# io.puts(prog)
|
265
|
+
# io.close_write
|
266
|
+
# p io.read
|
267
|
+
# end
|
268
|
+
|
269
|
+
#test the tag with the "id3v2" program
|
270
|
+
def id3v2_prog_test(tag, written_tag)
|
271
|
+
return if PLATFORM =~ /win32/
|
272
|
+
return if `which id3v2`.empty?
|
273
|
+
start = false
|
274
|
+
id3v2_output = {}
|
275
|
+
`id3v2 -l #{TEMP_FILE}`.each do |line|
|
276
|
+
if line =~ /^id3v2 tag info/
|
277
|
+
start = true
|
278
|
+
next
|
279
|
+
end
|
280
|
+
next unless start
|
281
|
+
k, v = /^(.{4}) \(.+\): (.+)$/.match(line)[1,2]
|
282
|
+
case k
|
283
|
+
#COMM (Comments): ()[spa]: fmg
|
284
|
+
when "COMM"
|
285
|
+
v.sub!(/\(\)\[.{3}\]: (.+)/, '\1')
|
286
|
+
end
|
287
|
+
id3v2_output[k] = v
|
288
|
+
end
|
289
|
+
|
290
|
+
assert_equal( id3v2_output, written_tag, "id3v2 program output doesn't match")
|
291
|
+
end
|
292
|
+
|
293
|
+
def write_temp_file(tag)
|
294
|
+
Mp3Info.open(TEMP_FILE) do |mp3|
|
295
|
+
mp3.tag2.update(tag)
|
296
|
+
end
|
297
|
+
return Mp3Info.open(TEMP_FILE) { |m| m.tag2 }
|
298
|
+
#system("cp -v #{TEMP_FILE} #{TEMP_FILE}.test")
|
299
|
+
end
|
300
|
+
|
301
|
+
def random_string(size)
|
302
|
+
out = ""
|
303
|
+
size.times { out << rand(256).chr }
|
304
|
+
out
|
305
|
+
end
|
306
|
+
|
307
|
+
=begin
|
308
|
+
|
309
|
+
def test_encoder
|
310
|
+
write_to_temp
|
311
|
+
info = Mp3Info.new(TEMP_FILE)
|
312
|
+
assert(info.encoder == "Lame 3.93")
|
313
|
+
end
|
314
|
+
|
315
|
+
def test_vbr
|
316
|
+
mp3_vbr = Base64.decode64 <<EOF
|
317
|
+
|
318
|
+
EOF
|
319
|
+
File.open(TEMP_FILE, "w") { |f| f.write(mp3_vbr) }
|
320
|
+
info = Mp3Info.new(TEMP_FILE)
|
321
|
+
assert_equal(info.vbr, true)
|
322
|
+
assert_equal(info.bitrate, 128)
|
323
|
+
end
|
324
|
+
=end
|
325
|
+
end
|