apetag 1.0.0

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