mp3info 0.6.17
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/.gitignore +12 -0
- data/History.txt +153 -0
- data/README.rdoc +68 -0
- data/lib/mp3info.rb +714 -0
- data/lib/mp3info/extension_modules.rb +46 -0
- data/lib/mp3info/id3v2.rb +437 -0
- data/mp3info.gemspec +17 -0
- data/test/fixtures.yml +86 -0
- data/test/test_ruby-mp3info.rb +572 -0
- metadata +76 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# coding:utf-8
|
|
2
|
+
# License:: Ruby
|
|
3
|
+
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
|
4
|
+
# Website:: http://ruby-mp3info.rubyforge.org/
|
|
5
|
+
|
|
6
|
+
class Mp3Info
|
|
7
|
+
module HashKeys #:nodoc:
|
|
8
|
+
### lets you specify hash["key"] as hash.key
|
|
9
|
+
### this came from CodingInRuby on RubyGarden
|
|
10
|
+
### http://www.rubygarden.org/ruby?CodingInRuby
|
|
11
|
+
def method_missing(meth,*args)
|
|
12
|
+
m = meth.id2name
|
|
13
|
+
if /=$/ =~ m
|
|
14
|
+
self[m.chop] = (args.length<2 ? args[0] : args)
|
|
15
|
+
else
|
|
16
|
+
self[m]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ::String
|
|
22
|
+
if RUBY_VERSION < "1.9.0"
|
|
23
|
+
alias getbyte []
|
|
24
|
+
else
|
|
25
|
+
def getbyte(i)
|
|
26
|
+
self[i].ord unless self[i].nil?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module Mp3FileMethods #:nodoc:
|
|
32
|
+
if RUBY_VERSION < "1.9.0"
|
|
33
|
+
def getbyte
|
|
34
|
+
getc
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def get32bits
|
|
39
|
+
(getbyte << 24) + (getbyte << 16) + (getbyte << 8) + getbyte
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def get_syncsafe
|
|
43
|
+
(getbyte << 21) + (getbyte << 14) + (getbyte << 7) + getbyte
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# coding:utf-8
|
|
2
|
+
# License:: Ruby
|
|
3
|
+
# Author:: Guillaume Pierronnet (mailto:moumar_AT__rubyforge_DOT_org)
|
|
4
|
+
# Website:: http://ruby-mp3info.rubyforge.org/
|
|
5
|
+
|
|
6
|
+
require "delegate"
|
|
7
|
+
require "iconv"
|
|
8
|
+
|
|
9
|
+
require "mp3info/extension_modules"
|
|
10
|
+
|
|
11
|
+
class ID3v2Error < StandardError ; end
|
|
12
|
+
|
|
13
|
+
# This class can be used to decode id3v2 tags from files, like .mp3 or .ape for example.
|
|
14
|
+
# It works like a hash, where key represents the tag name as 3 or 4 upper case letters
|
|
15
|
+
# (respectively related to 2.2 and 2.3+ tag) and value represented as array or raw value.
|
|
16
|
+
# Written version is always 2.3.
|
|
17
|
+
class ID3v2 < DelegateClass(Hash)
|
|
18
|
+
|
|
19
|
+
# Major version used when writing tags
|
|
20
|
+
WRITE_VERSION = 3
|
|
21
|
+
|
|
22
|
+
TAGS = {
|
|
23
|
+
"AENC" => "Audio encryption",
|
|
24
|
+
"APIC" => "Attached picture",
|
|
25
|
+
"COMM" => "Comments",
|
|
26
|
+
"COMR" => "Commercial frame",
|
|
27
|
+
"ENCR" => "Encryption method registration",
|
|
28
|
+
"EQUA" => "Equalization",
|
|
29
|
+
"ETCO" => "Event timing codes",
|
|
30
|
+
"GEOB" => "General encapsulated object",
|
|
31
|
+
"GRID" => "Group identification registration",
|
|
32
|
+
"IPLS" => "Involved people list",
|
|
33
|
+
"LINK" => "Linked information",
|
|
34
|
+
"MCDI" => "Music CD identifier",
|
|
35
|
+
"MLLT" => "MPEG location lookup table",
|
|
36
|
+
"OWNE" => "Ownership frame",
|
|
37
|
+
"PRIV" => "Private frame",
|
|
38
|
+
"PCNT" => "Play counter",
|
|
39
|
+
"POPM" => "Popularimeter",
|
|
40
|
+
"POSS" => "Position synchronisation frame",
|
|
41
|
+
"RBUF" => "Recommended buffer size",
|
|
42
|
+
"RVAD" => "Relative volume adjustment",
|
|
43
|
+
"RVRB" => "Reverb",
|
|
44
|
+
"SYLT" => "Synchronized lyric/text",
|
|
45
|
+
"SYTC" => "Synchronized tempo codes",
|
|
46
|
+
"TALB" => "Album/Movie/Show title",
|
|
47
|
+
"TBPM" => "BPM (beats per minute)",
|
|
48
|
+
"TCOM" => "Composer",
|
|
49
|
+
"TCON" => "Content type",
|
|
50
|
+
"TCOP" => "Copyright message",
|
|
51
|
+
"TDAT" => "Date",
|
|
52
|
+
"TDLY" => "Playlist delay",
|
|
53
|
+
"TENC" => "Encoded by",
|
|
54
|
+
"TEXT" => "Lyricist/Text writer",
|
|
55
|
+
"TFLT" => "File type",
|
|
56
|
+
"TIME" => "Time",
|
|
57
|
+
"TIT1" => "Content group description",
|
|
58
|
+
"TIT2" => "Title/songname/content description",
|
|
59
|
+
"TIT3" => "Subtitle/Description refinement",
|
|
60
|
+
"TKEY" => "Initial key",
|
|
61
|
+
"TLAN" => "Language(s)",
|
|
62
|
+
"TLEN" => "Length",
|
|
63
|
+
"TMED" => "Media type",
|
|
64
|
+
"TOAL" => "Original album/movie/show title",
|
|
65
|
+
"TOFN" => "Original filename",
|
|
66
|
+
"TOLY" => "Original lyricist(s)/text writer(s)",
|
|
67
|
+
"TOPE" => "Original artist(s)/performer(s)",
|
|
68
|
+
"TORY" => "Original release year",
|
|
69
|
+
"TOWN" => "File owner/licensee",
|
|
70
|
+
"TPE1" => "Lead performer(s)/Soloist(s)",
|
|
71
|
+
"TPE2" => "Band/orchestra/accompaniment",
|
|
72
|
+
"TPE3" => "Conductor/performer refinement",
|
|
73
|
+
"TPE4" => "Interpreted, remixed, or otherwise modified by",
|
|
74
|
+
"TPOS" => "Part of a set",
|
|
75
|
+
"TPUB" => "Publisher",
|
|
76
|
+
"TRCK" => "Track number/Position in set",
|
|
77
|
+
"TRDA" => "Recording dates",
|
|
78
|
+
"TRSN" => "Internet radio station name",
|
|
79
|
+
"TRSO" => "Internet radio station owner",
|
|
80
|
+
"TSIZ" => "Size",
|
|
81
|
+
"TSRC" => "ISRC (international standard recording code)",
|
|
82
|
+
"TSSE" => "Software/Hardware and settings used for encoding",
|
|
83
|
+
"TYER" => "Year",
|
|
84
|
+
"TXXX" => "User defined text information frame",
|
|
85
|
+
"UFID" => "Unique file identifier",
|
|
86
|
+
"USER" => "Terms of use",
|
|
87
|
+
"USLT" => "Unsychronized lyric/text transcription",
|
|
88
|
+
"WCOM" => "Commercial information",
|
|
89
|
+
"WCOP" => "Copyright/Legal information",
|
|
90
|
+
"WOAF" => "Official audio file webpage",
|
|
91
|
+
"WOAR" => "Official artist/performer webpage",
|
|
92
|
+
"WOAS" => "Official audio source webpage",
|
|
93
|
+
"WORS" => "Official internet radio station homepage",
|
|
94
|
+
"WPAY" => "Payment",
|
|
95
|
+
"WPUB" => "Publishers official webpage",
|
|
96
|
+
"WXXX" => "User defined URL link frame"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Translate V2 to V3 tags
|
|
100
|
+
TAG_MAPPING_2_2_to_2_3 = {
|
|
101
|
+
"BUF" => "RBUF",
|
|
102
|
+
"COM" => "COMM",
|
|
103
|
+
"CRA" => "AENC",
|
|
104
|
+
"EQU" => "EQUA",
|
|
105
|
+
"ETC" => "ETCO",
|
|
106
|
+
"GEO" => "GEOB",
|
|
107
|
+
"MCI" => "MCDI",
|
|
108
|
+
"MLL" => "MLLT",
|
|
109
|
+
"PIC" => "APIC",
|
|
110
|
+
"POP" => "POPM",
|
|
111
|
+
"REV" => "RVRB",
|
|
112
|
+
"RVA" => "RVAD",
|
|
113
|
+
"SLT" => "SYLT",
|
|
114
|
+
"STC" => "SYTC",
|
|
115
|
+
"TAL" => "TALB",
|
|
116
|
+
"TBP" => "TBPM",
|
|
117
|
+
"TCM" => "TCOM",
|
|
118
|
+
"TCO" => "TCON",
|
|
119
|
+
"TCR" => "TCOP",
|
|
120
|
+
"TDA" => "TDAT",
|
|
121
|
+
"TDY" => "TDLY",
|
|
122
|
+
"TEN" => "TENC",
|
|
123
|
+
"TFT" => "TFLT",
|
|
124
|
+
"TIM" => "TIME",
|
|
125
|
+
"TKE" => "TKEY",
|
|
126
|
+
"TLA" => "TLAN",
|
|
127
|
+
"TLE" => "TLEN",
|
|
128
|
+
"TMT" => "TMED",
|
|
129
|
+
"TOA" => "TOPE",
|
|
130
|
+
"TOF" => "TOFN",
|
|
131
|
+
"TOL" => "TOLY",
|
|
132
|
+
"TOR" => "TORY",
|
|
133
|
+
"TOT" => "TOAL",
|
|
134
|
+
"TP1" => "TPE1",
|
|
135
|
+
"TP2" => "TPE2",
|
|
136
|
+
"TP3" => "TPE3",
|
|
137
|
+
"TP4" => "TPE4",
|
|
138
|
+
"TPA" => "TPOS",
|
|
139
|
+
"TPB" => "TPUB",
|
|
140
|
+
"TRC" => "TSRC",
|
|
141
|
+
"TRD" => "TRDA",
|
|
142
|
+
"TRK" => "TRCK",
|
|
143
|
+
"TSI" => "TSIZ",
|
|
144
|
+
"TSS" => "TSSE",
|
|
145
|
+
"TT1" => "TIT1",
|
|
146
|
+
"TT2" => "TIT2",
|
|
147
|
+
"TT3" => "TIT3",
|
|
148
|
+
"TXT" => "TEXT",
|
|
149
|
+
"TXX" => "TXXX",
|
|
150
|
+
"TYE" => "TYER",
|
|
151
|
+
"UFI" => "UFID",
|
|
152
|
+
"ULT" => "USLT",
|
|
153
|
+
"WAF" => "WOAF",
|
|
154
|
+
"WAR" => "WOAR",
|
|
155
|
+
"WAS" => "WOAS",
|
|
156
|
+
"WCM" => "WCOM",
|
|
157
|
+
"WCP" => "WCOP",
|
|
158
|
+
"WPB" => "WPB",
|
|
159
|
+
"WXX" => "WXXX",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# See id3v2.4.0-structure document, at section 4.
|
|
163
|
+
TEXT_ENCODINGS = ["iso-8859-1", "utf-16", "utf-16be", "utf-8"]
|
|
164
|
+
|
|
165
|
+
include Mp3Info::HashKeys
|
|
166
|
+
|
|
167
|
+
# this is the position in the file where the tag really ends
|
|
168
|
+
attr_reader :io_position
|
|
169
|
+
|
|
170
|
+
# :+lang+: for writing comments
|
|
171
|
+
#
|
|
172
|
+
# :+encoding+: one of the string of +TEXT_ENCODINGS+,
|
|
173
|
+
# used as a source and destination encoding respectively
|
|
174
|
+
# for read and write tag2 values.
|
|
175
|
+
attr_reader :options
|
|
176
|
+
|
|
177
|
+
# possible options are described above ('options' attribute)
|
|
178
|
+
# you can access this object like an hash, with [] and []= methods
|
|
179
|
+
# special cases are ["disc_number"] and ["disc_total"] mirroring TPOS attribute
|
|
180
|
+
def initialize(options = {})
|
|
181
|
+
@options = {
|
|
182
|
+
:lang => "ENG",
|
|
183
|
+
:encoding => "iso-8859-1"
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@options.update(options)
|
|
187
|
+
@text_encoding_index = TEXT_ENCODINGS.index(@options[:encoding])
|
|
188
|
+
|
|
189
|
+
unless @text_encoding_index
|
|
190
|
+
raise(ArgumentError, "bad id3v2 text encoding specified")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@hash = {}
|
|
194
|
+
#TAGS.keys.each { |k| @hash[k] = nil }
|
|
195
|
+
@hash_orig = {}
|
|
196
|
+
super(@hash)
|
|
197
|
+
@parsed = false
|
|
198
|
+
@version_maj = @version_min = nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# does this tag has been correctly read ?
|
|
202
|
+
def parsed?
|
|
203
|
+
@parsed
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# does this tag has been changed ?
|
|
207
|
+
def changed?
|
|
208
|
+
@hash_orig != @hash
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# full version of this tag (like "2.3.0") or nil
|
|
212
|
+
# if tag was not correctly read
|
|
213
|
+
def version
|
|
214
|
+
if @version_maj && @version_min
|
|
215
|
+
"2.#{@version_maj}.#{@version_min}"
|
|
216
|
+
else
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
### gets id3v2 tag information from io object (must support #seek() method)
|
|
222
|
+
def from_io(io)
|
|
223
|
+
@io = io
|
|
224
|
+
original_pos = @io.pos
|
|
225
|
+
@io.extend(Mp3Info::Mp3FileMethods)
|
|
226
|
+
version_maj, version_min, flags = @io.read(3).unpack("CCB4")
|
|
227
|
+
@unsync, ext_header, experimental, footer = (0..3).collect { |i| flags[i].chr == '1' }
|
|
228
|
+
raise(ID3v2Error, "can't find version_maj ('#{version_maj}')") unless [2, 3, 4].include?(version_maj)
|
|
229
|
+
@version_maj, @version_min = version_maj, version_min
|
|
230
|
+
@tag_length = @io.get_syncsafe
|
|
231
|
+
|
|
232
|
+
@parsed = true
|
|
233
|
+
begin
|
|
234
|
+
case @version_maj
|
|
235
|
+
when 2
|
|
236
|
+
read_id3v2_2_frames
|
|
237
|
+
when 3, 4
|
|
238
|
+
# seek past extended header if present
|
|
239
|
+
@io.seek(@io.get_syncsafe - 4, IO::SEEK_CUR) if ext_header
|
|
240
|
+
read_id3v2_3_frames
|
|
241
|
+
end
|
|
242
|
+
rescue ID3v2Error => e
|
|
243
|
+
warn("warning: id3v2 tag not fully parsed: #{e.message}")
|
|
244
|
+
end
|
|
245
|
+
@io_position = @io.pos
|
|
246
|
+
@tag_length = @io_position - original_pos
|
|
247
|
+
|
|
248
|
+
@hash_orig = @hash.dup
|
|
249
|
+
#no more reading
|
|
250
|
+
@io = nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# dump tag for writing. Version is always 2.#{WRITE_VERSION}.0.
|
|
254
|
+
def to_bin
|
|
255
|
+
#TODO handle of @tag2[TLEN"]
|
|
256
|
+
#TODO add of crc
|
|
257
|
+
#TODO add restrictions tag
|
|
258
|
+
|
|
259
|
+
tag = ""
|
|
260
|
+
@hash.each do |k, v|
|
|
261
|
+
next unless v
|
|
262
|
+
next if v.respond_to?("empty?") and v.empty?
|
|
263
|
+
|
|
264
|
+
# Automagically translate V2 to V3 tags
|
|
265
|
+
k = TAG_MAPPING_2_2_to_2_3[k] if TAG_MAPPING_2_2_to_2_3.has_key?(k)
|
|
266
|
+
|
|
267
|
+
# doesn't encode id3v2.2 tags, which have 3 characters
|
|
268
|
+
next if k.size != 4
|
|
269
|
+
|
|
270
|
+
# Output one flag for each array element, or one only if it's not an array
|
|
271
|
+
[v].flatten.each do |value|
|
|
272
|
+
data = encode_tag(k, value.to_s, WRITE_VERSION)
|
|
273
|
+
#data << "\x00"*2 #End of tag
|
|
274
|
+
|
|
275
|
+
tag << k[0,4] #4 characte max for a tag's key
|
|
276
|
+
#tag << to_syncsafe(data.size) #+1 because of the language encoding byte
|
|
277
|
+
size = data.size
|
|
278
|
+
if RUBY_VERSION >= "1.9.0"
|
|
279
|
+
size = data.dup.force_encoding("binary").size
|
|
280
|
+
end
|
|
281
|
+
tag << [size].pack("N") #+1 because of the language encoding byte
|
|
282
|
+
tag << "\x00"*2 #flags
|
|
283
|
+
tag << data
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
tag_str = "ID3"
|
|
288
|
+
#version_maj, version_min, unsync, ext_header, experimental, footer
|
|
289
|
+
tag_str << [ WRITE_VERSION, 0, "0000" ].pack("CCB4")
|
|
290
|
+
tag_str << [to_syncsafe(tag.size)].pack("N")
|
|
291
|
+
tag_str << tag
|
|
292
|
+
puts "tag in binary format: #{tag_str.inspect}" if $DEBUG
|
|
293
|
+
tag_str
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def encode_tag(name, value, version)
|
|
299
|
+
puts "encode_tag(#{name.inspect}, #{value.inspect}, #{version})" if $DEBUG
|
|
300
|
+
|
|
301
|
+
text_encoding_index = @text_encoding_index
|
|
302
|
+
if (name.index("T") == 0 || name == "COMM" ) && version == 3
|
|
303
|
+
# in id3v2.3 tags, there is only 2 encodings possible
|
|
304
|
+
transcoded_value = value
|
|
305
|
+
if text_encoding_index >= 2
|
|
306
|
+
begin
|
|
307
|
+
transcoded_value = Iconv.iconv(TEXT_ENCODINGS[1],
|
|
308
|
+
TEXT_ENCODINGS[text_encoding_index],
|
|
309
|
+
value).first
|
|
310
|
+
rescue Iconv::Failure
|
|
311
|
+
transcoded_value = value
|
|
312
|
+
end
|
|
313
|
+
text_encoding_index = 1
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
case name
|
|
318
|
+
when "COMM"
|
|
319
|
+
puts "encode COMM: enc: #{text_encoding_index}, lang: #{@options[:lang]}, str: #{transcoded_value.dump}" if $DEBUG
|
|
320
|
+
[ text_encoding_index , @options[:lang], 0, transcoded_value ].pack("ca3ca*")
|
|
321
|
+
when /^T/
|
|
322
|
+
text_encoding_index.chr + transcoded_value
|
|
323
|
+
else
|
|
324
|
+
value
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
### Read a tag from file and perform UNICODE translation if needed
|
|
329
|
+
def decode_tag(name, raw_value)
|
|
330
|
+
puts("decode_tag(#{name.inspect}, #{raw_value.inspect})") if $DEBUG
|
|
331
|
+
case name
|
|
332
|
+
when /^COM/
|
|
333
|
+
#FIXME improve this
|
|
334
|
+
encoding, lang, str = raw_value.unpack("ca3a*")
|
|
335
|
+
out = raw_value.split(0.chr).last
|
|
336
|
+
when /^T/
|
|
337
|
+
encoding = raw_value.getbyte(0) # language encoding (see TEXT_ENCODINGS constant)
|
|
338
|
+
out = raw_value[1..-1]
|
|
339
|
+
# we need to convert the string in order to match
|
|
340
|
+
# the requested encoding
|
|
341
|
+
if encoding && TEXT_ENCODINGS[encoding] && out && encoding != @text_encoding_index
|
|
342
|
+
begin
|
|
343
|
+
out = Iconv.iconv(@options[:encoding], TEXT_ENCODINGS[encoding], out).first
|
|
344
|
+
rescue Iconv::Failure
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
# remove padding zeros for textual tags
|
|
348
|
+
out.sub!(/\0*$/, '') unless out.nil?
|
|
349
|
+
return out
|
|
350
|
+
else
|
|
351
|
+
return raw_value
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
### reads id3 ver 2.3.x/2.4.x frames and adds the contents to @tag2 hash
|
|
356
|
+
### NOTE: the id3v2 header does not take padding zero's into consideration
|
|
357
|
+
def read_id3v2_3_frames
|
|
358
|
+
loop do # there are 2 ways to end the loop
|
|
359
|
+
name = @io.read(4)
|
|
360
|
+
if name.nil? || name.getbyte(0) == 0 || name == "MP3e" #bug caused by old tagging application "mp3ext" ( http://www.mutschler.de/mp3ext/ )
|
|
361
|
+
@io.seek(-4, IO::SEEK_CUR) # 1. find a padding zero,
|
|
362
|
+
seek_to_v2_end
|
|
363
|
+
break
|
|
364
|
+
else
|
|
365
|
+
if @version_maj == 4
|
|
366
|
+
size = @io.get_syncsafe
|
|
367
|
+
else
|
|
368
|
+
size = @io.get32bits
|
|
369
|
+
end
|
|
370
|
+
@io.seek(2, IO::SEEK_CUR) # skip flags
|
|
371
|
+
puts "name '#{name}' size #{size}" if $DEBUG
|
|
372
|
+
add_value_to_tag2(name, size)
|
|
373
|
+
end
|
|
374
|
+
break if @io.pos >= @tag_length # 2. reach length from header
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
### reads id3 ver 2.2.x frames and adds the contents to @tag2 hash
|
|
379
|
+
### NOTE: the id3v2 header does not take padding zero's into consideration
|
|
380
|
+
def read_id3v2_2_frames
|
|
381
|
+
loop do
|
|
382
|
+
name = @io.read(3)
|
|
383
|
+
if name.nil? || name.getbyte(0) == 0
|
|
384
|
+
@io.seek(-3, IO::SEEK_CUR)
|
|
385
|
+
seek_to_v2_end
|
|
386
|
+
break
|
|
387
|
+
else
|
|
388
|
+
size = (@io.getbyte << 16) + (@io.getbyte << 8) + @io.getbyte
|
|
389
|
+
add_value_to_tag2(name, size)
|
|
390
|
+
break if @io.pos >= @tag_length
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
### Add data to tag2["name"]
|
|
396
|
+
### read lang_encoding, decode data if unicode and
|
|
397
|
+
### create an array if the key already exists in the tag
|
|
398
|
+
def add_value_to_tag2(name, size)
|
|
399
|
+
puts "add_value_to_tag2" if $DEBUG
|
|
400
|
+
|
|
401
|
+
if size > 50_000_000
|
|
402
|
+
raise ID3v2Error, "tag size is > 50_000_000"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
data_io = @io.read(size)
|
|
406
|
+
data = decode_tag(name, data_io)
|
|
407
|
+
|
|
408
|
+
if self["TPOS"] =~ /(\d+)\s*\/\s*(\d+)/
|
|
409
|
+
self["disc_number"] = $1.to_i
|
|
410
|
+
self["disc_total"] = $2.to_i
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
if self.keys.include?(name)
|
|
414
|
+
unless self[name].is_a?(Array)
|
|
415
|
+
self[name] = [ self[name] ]
|
|
416
|
+
end
|
|
417
|
+
self[name] << data
|
|
418
|
+
else
|
|
419
|
+
self[name] = data
|
|
420
|
+
end
|
|
421
|
+
p data if $DEBUG
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
### runs thru @file one char at a time looking for best guess of first MPEG
|
|
425
|
+
### frame, which should be first 0xff byte after id3v2 padding zero's
|
|
426
|
+
def seek_to_v2_end
|
|
427
|
+
until @io.getbyte == 0xff
|
|
428
|
+
raise ID3v2Error, "got EOF before finding id3v2 end" if @io.eof?
|
|
429
|
+
end
|
|
430
|
+
@io.seek(-1, IO::SEEK_CUR)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
### convert an 32 integer to a syncsafe string
|
|
434
|
+
def to_syncsafe(num)
|
|
435
|
+
( (num<<3) & 0x7f000000 ) + ( (num<<2) & 0x7f0000 ) + ( (num<<1) & 0x7f00 ) + ( num & 0x7f )
|
|
436
|
+
end
|
|
437
|
+
end
|