apetag 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/apetag.rb +489 -0
  2. data/test/test_apetag.rb +422 -0
  3. metadata +45 -0
@@ -0,0 +1,489 @@
1
+ #!/usr/bin/env ruby
2
+ # This library implements a APEv2 parser/generator.
3
+ # If called from the command line, it prints out the contents of the APEv2 tag for the given filename arguments.
4
+ #
5
+ # ruby-apetag is a pure Ruby library for manipulating APEv2 (and ID3v1.1) tags.
6
+ # It aims for standards compliance with the APE spec (1). APEv2 is the standard
7
+ # tagging format for Musepack (.mpc) and Monkey's Audio files (.ape), and it can
8
+ # also be used with mp3s as an alternative to the mess that is ID3v2.x
9
+ # (technically, it can be used on any file type and is not limited to storing
10
+ # just audio file metadata).
11
+ #
12
+ # The module is in written in pure Ruby, so it should be useable on all
13
+ # platforms that Ruby supports. It has been tested on OpenBSD.
14
+ # The minimum python version required should be 1.8, but it has only been tested
15
+ # on 1.8.4. Modifying the code to work with previous version shouldn't be
16
+ # difficult, though there aren't any plans to do so.
17
+ #
18
+ # The library is complete, but it hasn't been used, so there may still be
19
+ # some bugs in it. I haven't found any bugs in my normal use of it that I
20
+ # haven't already fixed, though. RDoc documentation is available, as is
21
+ # Test::Unit based testing.
22
+ #
23
+ # General Use:
24
+ #
25
+ # require 'apetag'
26
+ # a = ApeTag.new('file.mp3')
27
+ # a.exists? # if it already has an APEv2 tag
28
+ # a.raw # the raw APEv2+ID3v1.1 tag already on the file
29
+ # a.fields # a hash of fields, keys are strings, values are list of strings
30
+ # a.pretty_print # string suitable for pretty printing
31
+ # a.update{|fields| fields['Artist']='Test Artist'; fields.delete('Year')}
32
+ # # Update the tag with the added/changed/deleted fields
33
+ # # Note that you should do: a.update{|fields| fields.replace('Test'=>'Test')}
34
+ # # and NOT: a.update{|fields| fields = {'Test'=>'Test'}}
35
+ # # You need to update/modify the fields given, not reassign it
36
+ # a.remove! # remove the APEv2 and ID3v1.1 tags.
37
+ #
38
+ # To run the tests for the library, run test_apetag.rb.
39
+ #
40
+ # If you find any bugs, would like additional documentation, or want to submit a
41
+ # patch, please use Rubyforge (http://rubyforge.org/projects/apetag/).
42
+ #
43
+ # The most current source code can be accessed via anonymous SVN at
44
+ # svn://suven.no-ip.org/ruby-apetag/. Note that the library isn't modified on a
45
+ # regular basis, so it is unlikely to be different from the latest release.
46
+ #
47
+ # (1) http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification
48
+ #
49
+ # Copyright (c) 2007 Jeremy Evans
50
+ #
51
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
52
+ # of this software and associated documentation files (the "Software"), to deal
53
+ # in the Software without restriction, including without limitation the rights
54
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
55
+ # copies of the Software, and to permit persons to whom the Software is
56
+ # furnished to do so, subject to the following conditions:
57
+ #
58
+ # The above copyright notice and this permission notice shall be included in
59
+ # all copies or substantial portions of the Software.
60
+ #
61
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
62
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
63
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
64
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
65
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
66
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
67
+ # SOFTWARE.
68
+
69
+ require 'set'
70
+
71
+ # Error raised by the library
72
+ class ApeTagError < StandardError
73
+ end
74
+
75
+ # The individual items in the APE tag.
76
+ # Because all items can contain a list of values, this is a subclass of Array.
77
+ class ApeItem < Array
78
+ MIN_SIZE = 11 # 4+4+2+1 (length, flags, minimum key length, key-value separator)
79
+ BAD_KEYS = Set.new(%w'id3 tag oggs mp+')
80
+ BAD_KEY_RE = Regexp.new("[\0-\x1f\x80-\xff]")
81
+ ITEM_TYPES = %w'utf8 binary external reserved'
82
+
83
+ attr_reader :read_only, :ape_type, :key, :key_downcased
84
+
85
+ # Creates an APE tag with the appropriate key and value.
86
+ # If value is a valid ApeItem, just updates the key.
87
+ # If value is an Array, creates an ApeItem with the key and all of its values.
88
+ # Otherwise, creates an ApeItem with the key and the singular value.
89
+ # Raise ApeTagError if key or or value is invalid.
90
+ def self.create(key, value)
91
+ if value.is_a?(self) && value.valid?
92
+ value.key = key
93
+ return value
94
+ end
95
+ value = [value] unless value.is_a?(Array)
96
+ new(key, value)
97
+ end
98
+
99
+ # Parse an ApeItem from the given data string starting at the provided offset.
100
+ # Check for validity and populate the object with the parsed data.
101
+ # Return the offset of the next item (or end of string).
102
+ # Raise ApeTagError if the parsed data is invalid.
103
+ def self.parse(data, offset)
104
+ length, flags = data[offset...(offset+8)].unpack('VN')
105
+ raise ApeTagError, "Invalid item length at offset #{offset}" if length + offset + MIN_SIZE > data.length
106
+ raise ApeTagError, "Invalid item flags at offset #{offset}" if flags > 7
107
+ key_end = data.index("\0", offset += 8)
108
+ raise ApeTagError, "Missing key-value separator at offset #{offset}" unless key_end
109
+ raise ApeTagError, "Invalid item length at offset #{offset}" if (next_item_start=length + key_end + 1) > data.length
110
+ item = ApeItem.new(data[offset...key_end], data[(key_end+1)...next_item_start].split("\0"))
111
+ item.read_only = flags & 1 > 0
112
+ item.ape_type = ITEM_TYPES[flags/2]
113
+ return [item, next_item_start]
114
+ end
115
+
116
+ # Set key and value.
117
+ # Set read_only to false and ape_type to utf8.
118
+ # Raise ApeTagError if key or value is invalid.
119
+ def initialize(key, value)
120
+ self.key = key
121
+ self.read_only = false
122
+ self.ape_type = ITEM_TYPES[0]
123
+ super(value)
124
+ raise ApeTagError, "Invalid item value encoding (non UTF-8)" unless valid_value?
125
+ end
126
+
127
+ # Set ape_type if valid, otherwise raise ApeTagError.
128
+ def ape_type=(type)
129
+ raise ApeTagError, "Invalid APE type" unless valid_ape_type?(type)
130
+ @ape_type=type
131
+ end
132
+
133
+ # Set key if valid, otherwise raise ApeTagError.
134
+ def key=(key)
135
+ raise ApeTagError, "Invalid APE key" unless valid_key?(key)
136
+ @key = key
137
+ @key_downcased = key.downcase
138
+ end
139
+
140
+ # The on disk representation of the entire ApeItem.
141
+ # Raise ApeTagError if ApeItem is invalid.
142
+ def raw
143
+ raise ApeTagError, "Invalid key, value, APE type, or Read-Only Flag" unless valid?
144
+ flags = ITEM_TYPES.index(ape_type) * 2 + (read_only ? 1 : 0)
145
+ sv = string_value
146
+ "#{[sv.length, flags].pack('VN')}#{key}\0#{sv}"
147
+ end
148
+
149
+ # Set read only flag if valid, otherwise raise ApeTagError.
150
+ def read_only=(flag)
151
+ raise ApeTagError, "Invalid Read-Only Flag" unless valid_read_only?(flag)
152
+ @read_only = flag
153
+ end
154
+
155
+ # The on disk representation of the ApeItem's values.
156
+ def string_value
157
+ join("\0")
158
+ end
159
+
160
+ # Check if current item is valid
161
+ def valid?
162
+ valid_ape_type?(ape_type) && valid_read_only?(read_only) && valid_key?(key) && valid_value?
163
+ end
164
+
165
+ # Check if given type is a valid APE type (a member of ApeItem::ITEM_TYPES).
166
+ def valid_ape_type?(type)
167
+ ITEM_TYPES.include?(type)
168
+ end
169
+
170
+ # Check if the given key is a valid APE key (string, 2 <= length <= 255, not in ApeItem::BAD_KEYS, not containing invalid characters).
171
+ def valid_key?(key)
172
+ key.is_a?(String) && key.length >= 2 && key.length <= 255 && !BAD_KEYS.include?(key.downcase) && key !~ BAD_KEY_RE
173
+ end
174
+
175
+ # Check if the given read only flag is valid (boolean).
176
+ def valid_read_only?(flag)
177
+ [true, false].include?(flag)
178
+ end
179
+
180
+ # Check if the string value is valid UTF-8.
181
+ def valid_value?
182
+ begin
183
+ string_value.unpack('U*') if ape_type == 'utf8' || ape_type == 'external'
184
+ rescue ArgumentError
185
+ false
186
+ else
187
+ true
188
+ end
189
+ end
190
+ end
191
+
192
+ # Contains all of the ApeItems found in the filename/file given.
193
+ # MAX_SIZE and MAX_ITEM_COUNT constants are recommended defaults, they can be
194
+ # increased if necessary.
195
+ class ApeTag
196
+ MAX_SIZE = 8192
197
+ MAX_ITEM_COUNT = 64
198
+ HEADER_FLAGS = "\x00\x00\x00\xA0"
199
+ FOOTER_FLAGS = "\x00\x00\x00\x80"
200
+ PREAMBLE = "APETAGEX\xD0\x07\x00\x00"
201
+ RECOMMENDED_KEYS = %w'Title Artist Album Year Comment Genre Track Subtitle
202
+ Publisher Conductor Composer Copyright Publicationright File EAN/UPC ISBN
203
+ Catalog LC Media Index Related ISRC Abstract Language Bibliography
204
+ Introplay Dummy' << 'Debut Album' << 'Record Date' << 'Record Location'
205
+ ID3_GENRES = 'Blues, Classic Rock, Country, Dance, Disco, Funk, Grunge,
206
+ Hip-Hop, Jazz, Metal, New Age, Oldies, Other, Pop, R & B, Rap, Reggae,
207
+ Rock, Techno, Industrial, Alternative, Ska, Death Metal, Prank, Soundtrack,
208
+ Euro-Techno, Ambient, Trip-Hop, Vocal, Jazz + Funk, Fusion, Trance,
209
+ Classical, Instrumental, Acid, House, Game, Sound Clip, Gospel, Noise,
210
+ Alternative Rock, Bass, Soul, Punk, Space, Meditative, Instrumental Pop,
211
+ Instrumental Rock, Ethnic, Gothic, Darkwave, Techno-Industrial, Electronic,
212
+ Pop-Fol, Eurodance, Dream, Southern Rock, Comedy, Cult, Gangsta, Top 40,
213
+ Christian Rap, Pop/Funk, Jungle, Native US, Cabaret, New Wave, Psychadelic,
214
+ Rave, Showtunes, Trailer, Lo-Fi, Tribal, Acid Punk, Acid Jazz, Polka,
215
+ Retro, Musical, Rock & Roll, Hard Rock, Folk, Folk-Rock, National Folk,
216
+ Swing, Fast Fusion, Bebop, Latin, Revival, Celtic, Bluegrass, Avantgarde,
217
+ Gothic Rock, Progressive Rock, Psychedelic Rock, Symphonic Rock, Slow Rock,
218
+ Big Band, Chorus, Easy Listening, Acoustic, Humour, Speech, Chanson, Opera,
219
+ Chamber Music, Sonata, Symphony, Booty Bass, Primus, Porn Groove, Satire,
220
+ Slow Jam, Club, Tango, Samba, Folklore, Ballad, Power Ballad, Rhytmic Soul,
221
+ Freestyle, Duet, Punk Rock, Drum Solo, Acapella, Euro-House, Dance Hall,
222
+ Goa, Drum & Bass, Club-House, Hardcore, Terror, Indie, BritPop, Negerpunk,
223
+ Polsk Punk, Beat, Christian Gangsta Rap, Heavy Metal, Black Metal,
224
+ Crossover, Contemporary Christian, Christian Rock, Merengue, Salsa,
225
+ Trash Meta, Anime, Jpop, Synthpop'.split(',').collect{|g| g.strip}
226
+ ID3_GENRES_HASH = Hash.new(255.chr)
227
+ ID3_GENRES.each_with_index{|g,i| ID3_GENRES_HASH[g] = i.chr }
228
+ FILE_OBJ_METHODS = %w'close seek read pos write truncate'
229
+ YEAR_RE = Regexp.new('\d{4}')
230
+ MP3_RE = Regexp.new('\.mp3\z')
231
+
232
+ @@check_id3 = true
233
+
234
+ attr_reader :filename, :file, :tag_size, :tag_start, :tag_data, :tag_header, :tag_footer, :tag_item_count, :check_id3
235
+
236
+ # Set whether to check for id3 tags by default on file objects (defaults to true)
237
+ def self.check_id3=(flag)
238
+ raise ApeTagError, "check_id3 must be boolean" unless [true, false].include?(flag)
239
+ @@check_id3 = flag
240
+ end
241
+
242
+ # Set the filename or file object to operate on. If the object has all methods
243
+ # in FILE_OBJ_METHODS, it is treated as a file, otherwise, it is treated as a filename.
244
+ # If the filename is invalid, Errno::ENOENT or Errno::EINVAL will probably be raised when calling methods.
245
+ # Optional argument check_id3 checks for ID3 tags.
246
+ # If check_id3 is not specified and filename is a file object, the ApeTag default is used.
247
+ # If check_id3 is not specified and filename is a filename, it checks for ID3 tags only if
248
+ # the filename ends with ".mp3".
249
+ # If files have APE tags but no ID3 tags, ID3 tags will never be added.
250
+ # If files have neither tag, check_id3 will decide whether to add an ID3 tag.
251
+ # If files have both tags, make sure check_id3 is true or it will miss both tags.
252
+ def initialize(filename, check_id3 = nil)
253
+ if FILE_OBJ_METHODS.each{|method| break unless filename.respond_to?(method)}
254
+ @file = filename
255
+ @check_id3 = check_id3.nil? ? @@check_id3 : check_id3
256
+ else
257
+ @filename = filename.to_s
258
+ @check_id3 = check_id3 unless check_id3.nil?
259
+ @check_id3 = !MP3_RE.match(@filename).nil? if @check_id3.nil?
260
+ end
261
+ end
262
+
263
+ # Check the file for an APE tag. Returns true or false. Raises ApeTagError for corrupt tags.
264
+ def exists?
265
+ @has_tag.nil? ? access_file('rb'){has_tag} : @has_tag
266
+ end
267
+
268
+ # Remove an APE tag from a file, if one exists.
269
+ # Returns true. Raises ApeTagError for corrupt tags.
270
+ def remove!
271
+ access_file('rb+'){file.truncate(tag_start) if has_tag}
272
+ @has_tag, @fields, @id3, @tag_size, @tag_start, @tag_data, @tag_header, @tag_footer, @tag_item_count = []
273
+ true
274
+ end
275
+
276
+ # A hash of ApeItems found in the file, or an empty hash if the file
277
+ # doesn't have an APE tag. Raises ApeTagError for corrupt tags.
278
+ def fields
279
+ @fields || access_file('rb'){get_fields}
280
+ end
281
+
282
+ # Pretty print tags, with one line per field, showing key and value.
283
+ def pretty_print
284
+ begin
285
+ fields.values.sort_by{|value| value.key_downcased}.collect{|value| "#{value.key}: #{value.join(', ')}"}.join("\n")
286
+ rescue ApeTagError
287
+ "CORRUPT TAG!"
288
+ rescue Errno::ENOENT, Errno::EINVAL
289
+ "FILE NOT FOUND!"
290
+ end
291
+ end
292
+
293
+ # The raw APEv2 + ID3v1.1 tag. If one or the other is empty that part will be missing.
294
+ # Raises ApeTagError for corrupt tags.
295
+ def raw
296
+ exists?
297
+ "#{tag_header}#{tag_data}#{tag_footer}#{id3}"
298
+ end
299
+
300
+ # Yields a hash of ApeItems found in the file, or an empty hash if the file
301
+ # doesn't have an APE tag. This hash should be modified (not reassigned) inside
302
+ # the block. An APEv2+ID3v1.1 tag with the new fields will overwrite the previous
303
+ # tag. If the file doesn't have an APEv2 tag, one will be created and appended to it.
304
+ # If the file doesn't have an ID3v1.1 tag, one will be generated from the ApeTag fields
305
+ # and appended to it. If the file already has an ID3v1.1 tag, the data in it is ignored,
306
+ # and it is overwritten. Raises ApeTagError if either the existing tag is invalid
307
+ # or the tag to be written would be invalid.
308
+ def update(&block)
309
+ access_file('rb+') do
310
+ yield get_fields
311
+ normalize_fields
312
+ update_id3
313
+ update_ape
314
+ write_tag
315
+ end
316
+ fields
317
+ end
318
+
319
+ private
320
+ # If working with a file object, yield the object.
321
+ # If working with a filename, open the file to be accessed using the correct mode,
322
+ # yield the file. Return the value returned by the block passed.
323
+ def access_file(how, &block)
324
+ if @filename
325
+ File.open(filename, how) do |file|
326
+ @file = file
327
+ return_value = yield
328
+ @file.close
329
+ @file = nil
330
+ return_value
331
+ end
332
+ else
333
+ yield
334
+ end
335
+ end
336
+
337
+ # If working with a filename, use the file system's size for that filename.
338
+ # If working with a file that has a size method (e.g. StringIO), call that.
339
+ # Otherwise, seek to the end of the file and return the position.
340
+ def file_size
341
+ if @filename
342
+ File.size(filename)
343
+ elsif file.respond_to?(:size)
344
+ file.size
345
+ else
346
+ file.seek(0, IO::SEEK_END) && file.pos
347
+ end
348
+ end
349
+
350
+ # Parse the raw tag data to get the tag fields (a hash of ApeItems), or an empty hash
351
+ # if the file has no APE tag.
352
+ def get_fields
353
+ return @fields if @fields
354
+ return @fields = Hash.new unless has_tag
355
+ ape_items = Hash.new
356
+ ape_item_keys = Set.new
357
+ offset = 0
358
+ last_possible_item_start = tag_data.length - ApeItem::MIN_SIZE
359
+ tag_item_count.times do
360
+ raise ApeTagError, "End of tag reached but more items specified" if offset > last_possible_item_start
361
+ item, offset = ApeItem.parse(tag_data, offset)
362
+ raise ApeTagError, "Multiple items with same key (#{item.key.inspect})" if ape_item_keys.include?(item.key_downcased)
363
+ ape_item_keys.add(item.key_downcased)
364
+ ape_items[item.key] = item
365
+ end
366
+ raise ApeTagError, "Data remaing after specified number of items parsed" if offset != tag_data.length
367
+ @fields = ape_items
368
+ end
369
+
370
+ # Get various information about the tag (if it exists), and check it for validity if a tag is present.
371
+ def get_tag_information
372
+ unless file_size >= id3.length + 64
373
+ @has_tag = false
374
+ @tag_start = file_size - id3.length
375
+ return
376
+ end
377
+ file.seek(-32-id3.length, IO::SEEK_END)
378
+ tag_footer = file.read(32)
379
+ unless tag_footer[0...12] == PREAMBLE && tag_footer[20...24] == FOOTER_FLAGS
380
+ @has_tag = false
381
+ @tag_start = file_size - id3.length
382
+ return
383
+ end
384
+ @tag_footer = tag_footer
385
+ @tag_size, @tag_item_count = tag_footer[12...20].unpack('VV')
386
+ @tag_size += 32
387
+ raise ApeTagError, "Tag size (#{tag_size}) smaller than minimum size" if tag_size < 64
388
+ raise ApeTagError, "Tag size (#{tag_size}) larger than possible" if tag_size + id3.length > file_size
389
+ raise ApeTagError, "Tag size (#{tag_size}) is larger than #{MAX_SIZE}" if tag_size > MAX_SIZE
390
+ raise ApeTagError, "Item count (#{tag_item_count}) is larger than #{MAX_ITEM_COUNT}" if tag_item_count > MAX_ITEM_COUNT
391
+ raise ApeTagError, "Item count (#{tag_item_count}) is larger than possible" if tag_item_count > tag_size-64/ApeItem::MIN_SIZE
392
+ file.seek(-tag_size-id3.length, IO::SEEK_END)
393
+ @tag_start=file.pos
394
+ @tag_header=file.read(32)
395
+ @tag_data=file.read(tag_size-64)
396
+ raise ApeTagError, "Missing header" unless tag_header[0...12] == PREAMBLE && tag_header[20...24] == HEADER_FLAGS
397
+ raise ApeTagError, "Header and footer size does match" unless tag_size == tag_header[12...16].unpack('V')[0] + 32
398
+ raise ApeTagError, "Header and footer item count does match" unless tag_item_count == tag_header[16...20].unpack('V')[0]
399
+ @has_tag = true
400
+ end
401
+
402
+ # Check if the file has a tag or not
403
+ def has_tag
404
+ return @has_tag unless @has_tag.nil?
405
+ get_tag_information
406
+ @has_tag
407
+ end
408
+
409
+ # Get the raw id3 string for the file (this is ignored).
410
+ # If check_id3 is false, it doesn't check for the ID3, which means that
411
+ # the APE tag will probably not be recognized if the file ends with an ID3 tag.
412
+ def id3
413
+ return @id3 unless @id3.nil?
414
+ return @id3 = '' if file_size < 128 || check_id3 == false
415
+ file.seek(-128, IO::SEEK_END)
416
+ data = file.read(128)
417
+ @id3 = data[0...3] == 'TAG' && data[125] == 0 ? data : ''
418
+ end
419
+
420
+ # Turn fields hash from a hash of arbitrary objects to a hash of ApeItems
421
+ # Check that multiple identical keys are not present.
422
+ def normalize_fields
423
+ new_fields = Hash.new
424
+ new_fields_keys = Set.new
425
+ fields.each do |key, value|
426
+ new_fields[key] = value = ApeItem.create(key, value)
427
+ raise ApeTagError, "Multiple items with same key (#{value.key.inspect})" if new_fields_keys.include?(value.key_downcased)
428
+ new_fields_keys.add(value.key_downcased)
429
+ end
430
+ @fields = new_fields
431
+ end
432
+
433
+ # Update internal variables to reflect the new APE tag. Check that produced
434
+ # tag is still valid.
435
+ def update_ape
436
+ entries = fields.values.collect{|value| value.raw}.sort{|a,b| x = a.length <=> b.length; x != 0 ? x : a <=> b}
437
+ @tag_data = entries.join
438
+ @tag_item_count = entries.length
439
+ @tag_size = tag_data.length + 64
440
+ base_start = "#{PREAMBLE}#{[tag_size-32, tag_item_count].pack('VV')}"
441
+ base_end = "\0"*8
442
+ @tag_header = "#{base_start}#{HEADER_FLAGS}#{base_end}"
443
+ @tag_footer = "#{base_start}#{FOOTER_FLAGS}#{base_end}"
444
+ raise ApeTagError, "Updated tag has too many items (#{tag_item_count})" if tag_item_count > MAX_ITEM_COUNT
445
+ raise ApeTagError, "Updated tag too large (#{tag_size})" if tag_size > MAX_SIZE
446
+ end
447
+
448
+ # Update the ID3v1.1 tag variable to use the fields from the APEv2 tag.
449
+ # If the file doesn't have an ID3 and the file already has an APE tag or
450
+ # check_id3 is not set, an ID3 won't be added.
451
+ def update_id3
452
+ return if id3.length == 0 && (has_tag || check_id3 == false)
453
+ id3_fields = Hash.new('')
454
+ id3_fields['genre'] = 255.chr
455
+ fields.values.each do |value|
456
+ case value.key_downcased
457
+ when /\Atrack/
458
+ id3_fields['track'] = value.string_value.to_i
459
+ id3_fields['track'] = 0 if id3_fields['track'] > 255
460
+ id3_fields['track'] = id3_fields['track'].chr
461
+ when /\Agenre/
462
+ id3_fields['genre'] = ID3_GENRES_HASH[value.first]
463
+ when /\Adate\z/
464
+ match = YEAR_RE.match(value.string_value)
465
+ id3_fields['year'] = match[0] if match
466
+ when /\A(title|artist|album|year|comment)\z/
467
+ id3_fields[value.key_downcased] = value.join(', ')
468
+ end
469
+ end
470
+ @id3 = ["TAG", id3_fields['title'], id3_fields['artist'], id3_fields['album'],
471
+ id3_fields['year'], id3_fields['comment'], "\0", id3_fields['track'],
472
+ id3_fields['genre']].pack("a3a30a30a30a4a28a1a1a1")
473
+ end
474
+
475
+ # Write the APEv2 and ID3v1.1 tags to disk.
476
+ def write_tag
477
+ file.seek(tag_start, IO::SEEK_SET)
478
+ file.write(raw)
479
+ file.truncate(file.pos)
480
+ @has_tag = true
481
+ end
482
+ end
483
+
484
+ # If called directly from the command line, treat all arguments as filenames, and pretty print the APE tag's fields for each filename.
485
+ if __FILE__ == $0
486
+ ARGV.each do |filename|
487
+ puts filename, '-'*filename.length, ApeTag.new(filename).pretty_print, ''
488
+ end
489
+ end
@@ -0,0 +1,422 @@
1
+ #!/usr/bin/env ruby
2
+ require 'apetag'
3
+ require 'stringio'
4
+ require 'test/unit'
5
+
6
+ EMPTY_APE_TAG = "APETAGEX\320\a\0\0 \0\0\0\0\0\0\0\0\0\0\240\0\0\0\0\0\0\0\0APETAGEX\320\a\0\0 \0\0\0\0\0\0\0\0\0\0\200\0\0\0\0\0\0\0\0TAG\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\377"
7
+ EXAMPLE_APE_TAG = "APETAGEX\xd0\x07\x00\x00\xb0\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00Track\x001\x04\x00\x00\x00\x00\x00\x00\x00Date\x002007\t\x00\x00\x00\x00\x00\x00\x00Comment\x00XXXX-0000\x0b\x00\x00\x00\x00\x00\x00\x00Title\x00Love Cheese\x0b\x00\x00\x00\x00\x00\x00\x00Artist\x00Test Artist\x16\x00\x00\x00\x00\x00\x00\x00Album\x00Test Album\x00Other AlbumAPETAGEX\xd0\x07\x00\x00\xb0\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00TAGLove Cheese\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Test Artist\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Test Album, Other Album\x00\x00\x00\x00\x00\x00\x002007XXXX-0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xff"
8
+ EXAMPLE_APE_TAG2 = "APETAGEX\xd0\x07\x00\x00\x99\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00Blah\x00Blah\x04\x00\x00\x00\x00\x00\x00\x00Date\x002007\t\x00\x00\x00\x00\x00\x00\x00Comment\x00XXXX-0000\x0b\x00\x00\x00\x00\x00\x00\x00Artist\x00Test Artist\x16\x00\x00\x00\x00\x00\x00\x00Album\x00Test Album\x00Other AlbumAPETAGEX\xd0\x07\x00\x00\x99\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00TAG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Test Artist\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Test Album, Other Album\x00\x00\x00\x00\x00\x00\x002007XXXX-0000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff"
9
+ EMPTY_APE_ONLY_TAG, EXAMPLE_APE_ONLY_TAG, EXAMPLE_APE_ONLY_TAG2 = [EMPTY_APE_TAG, EXAMPLE_APE_TAG, EXAMPLE_APE_TAG2].collect{|x|x[0...-128]}
10
+ EXAMPLE_APE_FIELDS = {"Track"=>["1"], "Comment"=>["XXXX-0000"], "Album"=>["Test Album", "Other Album"], "Title"=>["Love Cheese"], "Artist"=>["Test Artist"], "Date"=>["2007"]}
11
+ EXAMPLE_APE_FIELDS2 = {"Blah"=>["Blah"], "Comment"=>["XXXX-0000"], "Album"=>["Test Album", "Other Album"], "Artist"=>["Test Artist"], "Date"=>["2007"]}
12
+ EXAMPLE_APE_TAG_PRETTY_PRINT = "Album: Test Album, Other Album\nArtist: Test Artist\nComment: XXXX-0000\nDate: 2007\nTitle: Love Cheese\nTrack: 1"
13
+
14
+ class ApeTagTest < Test::Unit::TestCase
15
+ def get_ape_tag(f, check_id3)
16
+ f.is_a?(ApeTag) ? f : ApeTag.new(f, check_id3)
17
+ end
18
+
19
+ def get_size(x)
20
+ get_ape_tag(x, nil).send :file_size
21
+ end
22
+
23
+ def item_test(item, check_id3)
24
+ f = item
25
+ id3_size = check_id3 ? 128 : 0
26
+ size = get_size(f)
27
+ assert_equal false, get_ape_tag(f, check_id3).exists?
28
+ assert_equal size, get_size(f)
29
+ assert_equal true, get_ape_tag(f, check_id3).remove!
30
+ assert_equal size, get_size(f)
31
+ assert_equal "", get_ape_tag(f, check_id3).raw
32
+ assert_equal size, get_size(f)
33
+ assert_equal Hash.new, get_ape_tag(f, check_id3).fields
34
+ assert_equal size, get_size(f)
35
+ assert_equal Hash.new, get_ape_tag(f, check_id3).update{|x|}
36
+ assert_equal (size+=64+id3_size), get_size(f)
37
+ assert_equal true, get_ape_tag(f, check_id3).exists?
38
+ assert_equal size, get_size(f)
39
+ assert_equal (check_id3 ? EMPTY_APE_TAG : EMPTY_APE_ONLY_TAG), get_ape_tag(f, check_id3).raw, "#{item.inspect} #{check_id3}"
40
+ assert_equal size, get_size(f)
41
+ assert_equal Hash.new, get_ape_tag(f, check_id3).fields
42
+ assert_equal size, get_size(f)
43
+ assert_equal Hash.new, get_ape_tag(f, check_id3).update{|x|}
44
+ assert_equal size, get_size(f)
45
+ assert_equal true, get_ape_tag(f, check_id3).remove!
46
+ assert_equal (size-=64+id3_size), get_size(f)
47
+ assert_equal EXAMPLE_APE_FIELDS, get_ape_tag(f, check_id3).update{|x| x.replace(EXAMPLE_APE_FIELDS)}
48
+ assert_equal (size+=208+id3_size), get_size(f)
49
+ assert_equal true, get_ape_tag(f, check_id3).exists?
50
+ assert_equal size, get_size(f)
51
+ assert_equal (check_id3 ? EXAMPLE_APE_TAG : EXAMPLE_APE_ONLY_TAG), get_ape_tag(f, check_id3).raw
52
+ assert_equal EXAMPLE_APE_TAG_PRETTY_PRINT, get_ape_tag(f, check_id3).pretty_print
53
+ assert_equal size, get_size(f)
54
+ assert_equal EXAMPLE_APE_FIELDS, get_ape_tag(f, check_id3).fields
55
+ assert_equal size, get_size(f)
56
+ assert_equal EXAMPLE_APE_FIELDS, get_ape_tag(f, check_id3).update{|x|}
57
+ assert_equal size, get_size(f)
58
+ assert_equal EXAMPLE_APE_FIELDS2, get_ape_tag(f, check_id3).update {|x| x.delete('Track'); x.delete('Title'); x['Blah']='Blah'}
59
+ assert_equal (size-=23), get_size(f)
60
+ assert_equal true, get_ape_tag(f, check_id3).exists?
61
+ assert_equal size, get_size(f)
62
+ assert_equal (check_id3 ? EXAMPLE_APE_TAG2 : EXAMPLE_APE_ONLY_TAG2), get_ape_tag(f, check_id3).raw
63
+ assert_equal size, get_size(f)
64
+ assert_equal EXAMPLE_APE_FIELDS2, get_ape_tag(f, check_id3).fields
65
+ assert_equal size, get_size(f)
66
+ assert_equal EXAMPLE_APE_FIELDS2, get_ape_tag(f, check_id3).update{|x|}
67
+ assert_equal size, get_size(f)
68
+ assert_equal true, get_ape_tag(f, check_id3).remove!
69
+ assert_equal "", get_ape_tag(f, check_id3).raw
70
+ assert_equal (size-=185+id3_size), get_size(f)
71
+ end
72
+
73
+ # Test to make sure different file sizes don't cause any problems.
74
+ # Use both StringIOs, Files, and Strings.
75
+ # Use both a single ApeTag for each test and a new ApeTag for each to test
76
+ # that ApeTag state is created and saved correctly.
77
+ def test_blanks
78
+ filename = 'test.apetag'
79
+ File.new(filename,'wb').close
80
+ [0,1,63,64,65,127,128,129,191,192,193,8191,8192,8193].each do |x|
81
+ [true, false].each do |check_id3|
82
+ s = StringIO.new(' ' * x)
83
+ item_test(s, check_id3)
84
+ item_test(ApeTag.new(s, check_id3), check_id3)
85
+ f = File.new(filename,'rb+')
86
+ f.write(' ' * x)
87
+ item_test(f, check_id3)
88
+ item_test(ApeTag.new(f, check_id3), check_id3)
89
+ f.close()
90
+ item_test(filename, check_id3)
91
+ item_test(ApeTag.new(filename, check_id3), check_id3)
92
+ end
93
+ end
94
+ File.delete(filename)
95
+ end
96
+
97
+ # Test ApeItem instance methods
98
+ def test_ape_item
99
+ ai = ApeItem.new('BlaH', ['BlAh'])
100
+ # Test valid defaults
101
+ assert_equal ['BlAh'], ai
102
+ assert_equal false, ai.read_only
103
+ assert_equal 'utf8', ai.ape_type
104
+ assert_equal 'BlaH', ai.key
105
+ assert_equal 'blah', ai.key_downcased
106
+ assert_equal 'BlAh', ai.string_value
107
+ assert_equal "\04\0\0\0\0\0\0\0BlaH\0BlAh", ai.raw
108
+ assert_equal true, ai.valid?
109
+
110
+ # Test valid read_only settings
111
+ assert_nothing_raised{ai.read_only=true}
112
+ assert_nothing_raised{ai.read_only=false}
113
+ assert_raises(ApeTagError){ai.read_only=nil}
114
+ assert_raises(ApeTagError){ai.read_only='Blah'}
115
+ assert_equal true, ai.valid?
116
+
117
+ # Test valid ape_type settings
118
+ ApeItem::ITEM_TYPES.each{|type| assert_nothing_raised{ai.ape_type=type}}
119
+ assert_raises(ApeTagError){ai.ape_type='Blah'}
120
+
121
+ # Test valid key settings
122
+ ((("\0".."\x1f").to_a+("\x80".."\xff").to_a).collect{|x|"#{x} "} +
123
+ [nil, 1, '', 'x', 'x'*256]+ApeItem::BAD_KEYS.to_a).each{|x|assert_raises(ApeTagError){ai.key=x}}
124
+ ("\x20".."\x7f").to_a.collect{|x|"#{x} "}+ApeItem::BAD_KEYS.to_a.collect{|x|"#{x} "} +
125
+ ['xx', 'x'*255].each{|x| assert_nothing_raised{ai.key=x}}
126
+
127
+ # Test valid raw and string value for different settings
128
+ ai.key="BlaH"
129
+ assert_equal "\04\0\0\0\0\0\0\06BlaH\0BlAh", ai.raw
130
+ assert_equal 'BlAh', ai.string_value
131
+ ai.read_only=true
132
+ assert_equal "\04\0\0\0\0\0\0\07BlaH\0BlAh", ai.raw
133
+ assert_equal 'BlAh', ai.string_value
134
+ ai << 'XYZ'
135
+ assert_equal "\010\0\0\0\0\0\0\07BlaH\0BlAh\0XYZ", ai.raw
136
+ assert_equal "BlAh\0XYZ", ai.string_value
137
+ end
138
+
139
+ # Test ApeItem.create methods
140
+ def test_ape_item_create
141
+ ai = ApeItem.new('BlaH', ['BlAh'])
142
+ ac = ApeItem.create('BlaH', ai)
143
+ # Test same key and ApeItem passed gives same item with key
144
+ assert_equal ai.object_id, ac.object_id
145
+ assert_equal 'BlaH', ai.key
146
+ assert_equal 'BlaH', ac.key
147
+ # Test different key and ApeItem passed gives same item with different key
148
+ ac = ApeItem.create('XXX', ai)
149
+ assert_equal ai.object_id, ac.object_id
150
+ assert_equal 'XXX', ai.key
151
+ assert_equal 'XXX', ac.key
152
+
153
+ # Test create fails with invalid key
154
+ assert_raises(ApeTagError){ApeItem.create('', ai)}
155
+ # Test create fails with invalid UTF-8 value
156
+ assert_raises(ApeTagError){ApeItem.create('xx',["\xfe"])}
157
+ # Test create doesn't fail with valid UTF-8 value
158
+ assert_nothing_raised{ApeItem.create('xx',[[12345, 1345].pack('UU')])}
159
+
160
+ # Test create with empty array
161
+ ac = ApeItem.create('Blah', [])
162
+ assert_equal ApeItem, ac.class
163
+ assert_equal 0, ac.length
164
+ assert_equal '', ac.string_value
165
+
166
+ # Test create works with string
167
+ ac = ApeItem.create('Blah', 'Blah')
168
+ assert_equal ApeItem, ac.class
169
+ assert_equal 1, ac.length
170
+ assert_equal 'Blah', ac.string_value
171
+
172
+ # Test create works with random object (Hash in this case)
173
+ ac = ApeItem.create('Blah', 'xfe'=>132)
174
+ assert_equal ApeItem, ac.class
175
+ assert_equal 1, ac.length
176
+ assert_equal 'xfe132', ac.string_value
177
+
178
+ # Test create works with array of mixed objects
179
+ ac = ApeItem.create('Blah', ['sadf', 'adsfas', 11, {'xfe'=>132}])
180
+ assert_equal ApeItem, ac.class
181
+ assert_equal 4, ac.length
182
+ assert_equal "sadf\0adsfas\00011\0xfe132", ac.string_value
183
+ end
184
+
185
+ # Test ApeItem.parse
186
+ def test_ape_item_parse
187
+ data = "\010\0\0\0\0\0\0\07BlaH\0BlAh\0XYZ"
188
+ # Test simple item parsing
189
+ ai, offset = ApeItem.parse(data, 0)
190
+ assert_equal 2, ai.length
191
+ assert_equal offset, data.length
192
+ assert_equal "BlAh\0XYZ", ai.string_value
193
+ assert_equal true, ai.read_only
194
+ assert_equal 'reserved', ai.ape_type
195
+ assert_equal 'BlaH', ai.key
196
+
197
+ # Test parsing with bad key
198
+ assert_raises(ApeTagError){ApeItem.parse("\0\0\0\0\0\0\0\07x\0", 0)}
199
+
200
+ # Test parsing with no key end
201
+ assert_raises(ApeTagError){ApeItem.parse(data[0...-1], 0)}
202
+
203
+ # Test parsing with bad start value
204
+ assert_raises(ApeTagError){ApeItem.parse(data, 1)}
205
+
206
+ # Test parsing with bad/good flags
207
+ data[4] = 8
208
+ assert_raises(ApeTagError){ApeItem.parse(data, 0)}
209
+ data[4] = 0
210
+ assert_nothing_raised{ApeItem.parse(data, 0)}
211
+
212
+ # Test parsing with length longer than string
213
+ data[0] = 9
214
+ assert_raises(ApeTagError){ApeItem.parse(data, 0)}
215
+
216
+ # Test parsing with length shorter than string gives valid ApeItem
217
+ # Of course, the next item will probably be parsed incorrectly
218
+ data[0] = 3
219
+ assert_nothing_raised{ai, offset = ApeItem.parse(data, 0)}
220
+ assert_equal 16, offset
221
+ assert_equal "BlaH", ai.key
222
+ assert_equal "BlA", ai.string_value
223
+
224
+ # Test parsing gets correct key end
225
+ data[12] = '3'
226
+ assert_nothing_raised{ai, offset = ApeItem.parse(data, 0)}
227
+ assert_equal "BlaH3BlAh", ai.key
228
+ assert_equal "XYZ", ai.string_value
229
+
230
+ # Test parsing of invalid UTF-8
231
+ data = "\012\0\0\0\07\0\0\0BlaH\0BlAh\0XYZ\0\xff"
232
+ assert_raises(ApeTagError){ApeItem.parse(data, 0)}
233
+ end
234
+
235
+ # Test parsing of whole tags that have been monkeyed with
236
+ def test_bad_tags
237
+ data = EMPTY_APE_TAG.dup
238
+ # Test default case OK
239
+ assert_nothing_raised{ApeTag.new(StringIO.new(data)).raw}
240
+
241
+ # Test footer size less than minimum size (32)
242
+ data[44] = 31
243
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
244
+ data[44] = 0
245
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
246
+
247
+ # Test tag size > 8192, when both larger than file and smaller than file
248
+ data[44] = 225
249
+ data[45] = 31
250
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
251
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(' '*8192+data)).raw}
252
+
253
+ data = EMPTY_APE_TAG.dup
254
+ # Test unmatching header and footer tag size, with footer size wrong
255
+ data[44] = 33
256
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
257
+
258
+ # Test matching header and footer but size to large for file
259
+ data[12] = 33
260
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
261
+
262
+ # Test that header and footer size isn't too large for file, but doesn't
263
+ # find the header
264
+ data=" #{data}"
265
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
266
+
267
+ # Test unmatching header and footer tag size, with header size wrong
268
+ data[45] = 32
269
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
270
+
271
+ data = EMPTY_APE_TAG.dup
272
+ # Test item count greater than maximum (64)
273
+ data[48] = 65
274
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
275
+
276
+ # Test item count greater than possible given tag size
277
+ data[48] = 1
278
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
279
+
280
+ # Test unmatched header and footer item size, header size wrong
281
+ data[48] = 0
282
+ data[16] = 1
283
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).raw}
284
+
285
+ # Test unmatched header and footer item size, footer size wrong
286
+ data = EXAMPLE_APE_TAG.dup
287
+ data[48] -=1
288
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
289
+
290
+ # Test missing/corrupt header
291
+ data = EMPTY_APE_TAG.dup
292
+ data[0] = 0
293
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
294
+
295
+ # Test parsing bad first item size
296
+ data = EXAMPLE_APE_TAG.dup
297
+ data[32] +=1
298
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
299
+
300
+ # Test parsing bad first item invalid key
301
+ data = EXAMPLE_APE_TAG.dup
302
+ data[40] = 0
303
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
304
+
305
+ # Test parsing bad first item key end
306
+ data = EXAMPLE_APE_TAG.dup
307
+ data[45] = 1
308
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
309
+
310
+ # Test parsing bad second item length too long
311
+ data = EXAMPLE_APE_TAG.dup
312
+ data[47] = 255
313
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
314
+
315
+ # Test parsing case insensitive duplicate keys
316
+ data = EXAMPLE_APE_TAG.dup
317
+ data[40...45] = 'Album'
318
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
319
+ data[40...45] = 'album'
320
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
321
+ data[40...45] = 'ALBUM'
322
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
323
+
324
+ # Test parsing incorrect item counts
325
+ data = EXAMPLE_APE_TAG.dup
326
+ data[16] -= 1
327
+ data[192] -= 1
328
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
329
+ data[16] += 2
330
+ data[192] += 2
331
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
332
+
333
+ # Test updating with duplicate keys added
334
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['album']='blah'}}
335
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['ALBUM']='blah'}}
336
+ # Test updating without duplicate keys added
337
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album']='blah'}}
338
+
339
+ # Test updating an existing ApeItem via various array methods
340
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'] += ['blah']}}
341
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'] << 'blah'}}
342
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'].replace(['blah'])}}
343
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'].pop}}
344
+ assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'].shift}}
345
+
346
+ # Test updating with invalid value
347
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| x['Album']="\xfe"}}
348
+
349
+ # Test updating with invalid key
350
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| x['x']=""}}
351
+
352
+ # Test updating with too many items
353
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| 65.times{|i|x["blah#{i}"]=""}}}
354
+ # Test updating with just enough items
355
+ assert_nothing_raised{ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| 64.times{|i|x["blah#{i}"]=""}}}
356
+
357
+ # Test updating with too large a tag
358
+ assert_raises(ApeTagError){ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| x['xx']=' '*8118}}
359
+ # Test updating with just enough tag
360
+ assert_nothing_raised{ApeTag.new(StringIO.new(EMPTY_APE_TAG.dup)).update{|x| x['xx']=' '*8117}}
361
+ end
362
+
363
+ def test_check_id3
364
+ x = StringIO.new()
365
+ assert_equal 0, x.size
366
+
367
+ # Test ApeTag defaults to adding id3s on file objects without ape tags
368
+ ApeTag.new(x).update{}
369
+ assert_equal 192, x.size
370
+ # Test ApeTag doesn't find tag if not checking id3s and and id3 is present
371
+ ApeTag.new(x, false).remove!
372
+ assert_equal 192, x.size
373
+ # Test ApeTag doesn't add id3s if ape tag exists but id3 does not
374
+ x.truncate(64)
375
+ assert_equal 64, x.size
376
+ ApeTag.new(x).update{}
377
+ assert_equal 64, x.size
378
+ ApeTag.new(x).remove!
379
+ assert_equal 0, x.size
380
+
381
+ # Test ApeTag without checking doesn't add id3
382
+ ApeTag.new(x, false).update{}
383
+ assert_equal 64, x.size
384
+ ApeTag.new(x).remove!
385
+ assert_equal 0, x.size
386
+
387
+ # Test ApeTag with explicit check_id3 argument works
388
+ ApeTag.new(x, true).update{}
389
+ assert_equal 192, x.size
390
+ ApeTag.new(x, false).remove!
391
+ assert_equal 192, x.size
392
+ ApeTag.new(x).remove!
393
+ assert_equal 0, x.size
394
+
395
+ # Test whether check_id3 class variable works
396
+ ApeTag.check_id3 = false
397
+ ApeTag.new(x).update{}
398
+ assert_equal 64, x.size
399
+ ApeTag.new(x).remove!
400
+ assert_equal 0, x.size
401
+ ApeTag.check_id3 = true
402
+ assert_raises(ApeTagError){ApeTag.check_id3 = 0}
403
+
404
+ # Test non-mp3 filename defaults to no id3
405
+ filename = 'test.apetag'
406
+ File.new(filename,'wb').close
407
+ ApeTag.new(filename).update{}
408
+ assert_equal 64, get_size(filename)
409
+ ApeTag.new(filename).remove!
410
+ assert_equal 0, get_size(filename)
411
+ File.delete(filename)
412
+
413
+ # Test mp3 filename defaults to id3
414
+ filename = 'test.apetag.mp3'
415
+ File.new(filename,'wb').close
416
+ ApeTag.new(filename).update{}
417
+ assert_equal 192, get_size(filename)
418
+ ApeTag.new(filename).remove!
419
+ assert_equal 0, get_size(filename)
420
+ File.delete(filename)
421
+ end
422
+ end
metadata ADDED
@@ -0,0 +1,45 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: apetag
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2007-01-14 00:00:00 -08:00
8
+ summary: APEv2 Tag Parser/Generator
9
+ require_paths:
10
+ - .
11
+ email: jeremyevans0@gmail.com
12
+ homepage:
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: apetag
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Jeremy Evans
30
+ files:
31
+ - apetag.rb
32
+ test_files:
33
+ - test/test_apetag.rb
34
+ rdoc_options: []
35
+
36
+ extra_rdoc_files: []
37
+
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ requirements: []
43
+
44
+ dependencies: []
45
+