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.
@@ -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