apetag 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/apetag.rb +32 -38
- data/test/test_apetag.rb +6 -8
- metadata +13 -5
data/apetag.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
# This library implements a APEv2 parser/generator.
|
3
|
-
# If called from the command line, it prints out the contents of the APEv2 tag
|
3
|
+
# If called from the command line, it prints out the contents of the APEv2 tag
|
4
|
+
# for the given filename arguments.
|
4
5
|
#
|
5
|
-
# ruby-apetag is a pure Ruby library for manipulating APEv2
|
6
|
+
# ruby-apetag is a pure Ruby library for manipulating APEv2 tags.
|
6
7
|
# It aims for standards compliance with the APE spec (1). APEv2 is the standard
|
7
8
|
# tagging format for Musepack (.mpc) and Monkey's Audio files (.ape), and it can
|
8
|
-
# also be used with mp3s as an alternative to
|
9
|
-
#
|
10
|
-
# just audio file metadata).
|
9
|
+
# also be used with mp3s as an alternative to ID3v2.x (technically, it can be
|
10
|
+
# used on any file type and is not limited to storing just audio file metadata).
|
11
11
|
#
|
12
12
|
# The module is in written in pure Ruby, so it should be useable on all
|
13
|
-
# platforms that Ruby supports. It
|
13
|
+
# platforms that Ruby supports. It is developed and tested on OpenBSD.
|
14
14
|
# The minimum Ruby version required should be 1.8, but it has only been tested
|
15
|
-
# on 1.8.4
|
15
|
+
# on 1.8.4+. Modifying the code to work with previous version shouldn't be
|
16
16
|
# difficult, though there aren't any plans to do so.
|
17
17
|
#
|
18
18
|
# General Use:
|
@@ -20,8 +20,8 @@
|
|
20
20
|
# require 'apetag'
|
21
21
|
# a = ApeTag.new('file.mp3')
|
22
22
|
# a.exists? # if it already has an APEv2 tag
|
23
|
-
# a.raw # the raw APEv2+ID3v1.1 tag
|
24
|
-
# a.fields # a
|
23
|
+
# a.raw # the raw APEv2+ID3v1.1 tag string in the file
|
24
|
+
# a.fields # a CICPHash of fields, keys are strings, values are list of strings
|
25
25
|
# a.pretty_print # string suitable for pretty printing
|
26
26
|
# a.update{|fields| fields['Artist']='Test Artist'; fields.delete('Year')}
|
27
27
|
# # Update the tag with the added/changed/deleted fields
|
@@ -36,8 +36,8 @@
|
|
36
36
|
# patch, please use Rubyforge (http://rubyforge.org/projects/apetag/).
|
37
37
|
#
|
38
38
|
# The most current source code can be accessed via anonymous SVN at
|
39
|
-
# svn://
|
40
|
-
# regular basis, so it is unlikely to be different from the latest release.
|
39
|
+
# svn://code.jeremyevans.net/ruby-apetag/. Note that the library isn't modified
|
40
|
+
# on a regular basis, so it is unlikely to be different from the latest release.
|
41
41
|
#
|
42
42
|
# (1) http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification
|
43
43
|
#
|
@@ -62,6 +62,7 @@
|
|
62
62
|
# SOFTWARE.
|
63
63
|
|
64
64
|
require 'set'
|
65
|
+
require 'cicphash'
|
65
66
|
|
66
67
|
# Error raised by the library
|
67
68
|
class ApeTagError < StandardError
|
@@ -71,11 +72,10 @@ end
|
|
71
72
|
# Because all items can contain a list of values, this is a subclass of Array.
|
72
73
|
class ApeItem < Array
|
73
74
|
MIN_SIZE = 11 # 4+4+2+1 (length, flags, minimum key length, key-value separator)
|
74
|
-
|
75
|
-
BAD_KEY_RE = Regexp.new("[\0-\x1f\x80-\xff]")
|
75
|
+
BAD_KEY_RE = /[\0-\x1f\x80-\xff]|\A(?:id3|tag|oggs|mp\+)\z/i
|
76
76
|
ITEM_TYPES = %w'utf8 binary external reserved'
|
77
77
|
|
78
|
-
attr_reader :read_only, :ape_type, :key
|
78
|
+
attr_reader :read_only, :ape_type, :key
|
79
79
|
|
80
80
|
# Creates an APE tag with the appropriate key and value.
|
81
81
|
# If value is a valid ApeItem, just updates the key.
|
@@ -129,7 +129,6 @@ class ApeItem < Array
|
|
129
129
|
def key=(key)
|
130
130
|
raise ApeTagError, "Invalid APE key" unless valid_key?(key)
|
131
131
|
@key = key
|
132
|
-
@key_downcased = key.downcase
|
133
132
|
end
|
134
133
|
|
135
134
|
# The on disk representation of the entire ApeItem.
|
@@ -162,9 +161,9 @@ class ApeItem < Array
|
|
162
161
|
ITEM_TYPES.include?(type)
|
163
162
|
end
|
164
163
|
|
165
|
-
# Check if the given key is a valid APE key (string, 2 <= length <= 255, not
|
164
|
+
# Check if the given key is a valid APE key (string, 2 <= length <= 255, not containing invalid characters or keys).
|
166
165
|
def valid_key?(key)
|
167
|
-
key.is_a?(String) && key.length >= 2 && key.length <= 255 &&
|
166
|
+
key.is_a?(String) && key.length >= 2 && key.length <= 255 && key !~ BAD_KEY_RE
|
168
167
|
end
|
169
168
|
|
170
169
|
# Check if the given read only flag is valid (boolean).
|
@@ -218,7 +217,7 @@ class ApeTag
|
|
218
217
|
Polsk Punk, Beat, Christian Gangsta Rap, Heavy Metal, Black Metal,
|
219
218
|
Crossover, Contemporary Christian, Christian Rock, Merengue, Salsa,
|
220
219
|
Trash Meta, Anime, Jpop, Synthpop'.split(',').collect{|g| g.strip}
|
221
|
-
ID3_GENRES_HASH =
|
220
|
+
ID3_GENRES_HASH = CICPHash.new(255.chr)
|
222
221
|
ID3_GENRES.each_with_index{|g,i| ID3_GENRES_HASH[g] = i.chr }
|
223
222
|
FILE_OBJ_METHODS = %w'close seek read pos write truncate'
|
224
223
|
YEAR_RE = Regexp.new('\d{4}')
|
@@ -268,7 +267,7 @@ class ApeTag
|
|
268
267
|
true
|
269
268
|
end
|
270
269
|
|
271
|
-
# A
|
270
|
+
# A CICPHash of ApeItems found in the file, or an empty CICPHash if the file
|
272
271
|
# doesn't have an APE tag. Raises ApeTagError for corrupt tags.
|
273
272
|
def fields
|
274
273
|
@fields || access_file('rb'){get_fields}
|
@@ -277,7 +276,7 @@ class ApeTag
|
|
277
276
|
# Pretty print tags, with one line per field, showing key and value.
|
278
277
|
def pretty_print
|
279
278
|
begin
|
280
|
-
fields.values.sort_by{|value| value.
|
279
|
+
fields.values.sort_by{|value| value.key}.collect{|value| "#{value.key}: #{value.join(', ')}"}.join("\n")
|
281
280
|
rescue ApeTagError
|
282
281
|
"CORRUPT TAG!"
|
283
282
|
rescue Errno::ENOENT, Errno::EINVAL
|
@@ -292,7 +291,7 @@ class ApeTag
|
|
292
291
|
"#{tag_header}#{tag_data}#{tag_footer}#{id3}"
|
293
292
|
end
|
294
293
|
|
295
|
-
# Yields a
|
294
|
+
# Yields a CICPHash of ApeItems found in the file, or an empty CICPHash if the file
|
296
295
|
# doesn't have an APE tag. This hash should be modified (not reassigned) inside
|
297
296
|
# the block. An APEv2+ID3v1.1 tag with the new fields will overwrite the previous
|
298
297
|
# tag. If the file doesn't have an APEv2 tag, one will be created and appended to it.
|
@@ -346,16 +345,14 @@ class ApeTag
|
|
346
345
|
# if the file has no APE tag.
|
347
346
|
def get_fields
|
348
347
|
return @fields if @fields
|
349
|
-
return @fields =
|
350
|
-
ape_items =
|
351
|
-
ape_item_keys = Set.new
|
348
|
+
return @fields = CICPHash.new unless has_tag
|
349
|
+
ape_items = CICPHash.new
|
352
350
|
offset = 0
|
353
351
|
last_possible_item_start = tag_data.length - ApeItem::MIN_SIZE
|
354
352
|
tag_item_count.times do
|
355
353
|
raise ApeTagError, "End of tag reached but more items specified" if offset > last_possible_item_start
|
356
354
|
item, offset = ApeItem.parse(tag_data, offset)
|
357
|
-
raise ApeTagError, "Multiple items with same key (#{item.key.inspect})" if
|
358
|
-
ape_item_keys.add(item.key_downcased)
|
355
|
+
raise ApeTagError, "Multiple items with same key (#{item.key.inspect})" if ape_items.include?(item.key)
|
359
356
|
ape_items[item.key] = item
|
360
357
|
end
|
361
358
|
raise ApeTagError, "Data remaining after specified number of items parsed" if offset != tag_data.length
|
@@ -415,12 +412,9 @@ class ApeTag
|
|
415
412
|
# Turn fields hash from a hash of arbitrary objects to a hash of ApeItems
|
416
413
|
# Check that multiple identical keys are not present.
|
417
414
|
def normalize_fields
|
418
|
-
new_fields =
|
419
|
-
new_fields_keys = Set.new
|
415
|
+
new_fields = CICPHash.new
|
420
416
|
fields.each do |key, value|
|
421
|
-
new_fields[key] =
|
422
|
-
raise ApeTagError, "Multiple items with same key (#{value.key.inspect})" if new_fields_keys.include?(value.key_downcased)
|
423
|
-
new_fields_keys.add(value.key_downcased)
|
417
|
+
new_fields[key] = ApeItem.create(key, value)
|
424
418
|
end
|
425
419
|
@fields = new_fields
|
426
420
|
end
|
@@ -445,21 +439,21 @@ class ApeTag
|
|
445
439
|
# check_id3 is not set, an ID3 won't be added.
|
446
440
|
def update_id3
|
447
441
|
return if id3.length == 0 && (has_tag || check_id3 == false)
|
448
|
-
id3_fields =
|
442
|
+
id3_fields = CICPHash.new('')
|
449
443
|
id3_fields['genre'] = 255.chr
|
450
444
|
fields.values.each do |value|
|
451
|
-
case value.
|
452
|
-
when /\Atrack/
|
445
|
+
case value.key
|
446
|
+
when /\Atrack/i
|
453
447
|
id3_fields['track'] = value.string_value.to_i
|
454
448
|
id3_fields['track'] = 0 if id3_fields['track'] > 255
|
455
449
|
id3_fields['track'] = id3_fields['track'].chr
|
456
|
-
when /\Agenre/
|
450
|
+
when /\Agenre/i
|
457
451
|
id3_fields['genre'] = ID3_GENRES_HASH[value.first]
|
458
|
-
when /\Adate\z/
|
452
|
+
when /\Adate\z/i
|
459
453
|
match = YEAR_RE.match(value.string_value)
|
460
454
|
id3_fields['year'] = match[0] if match
|
461
|
-
when /\A(title|artist|album|year|comment)\z/
|
462
|
-
id3_fields[value.
|
455
|
+
when /\A(title|artist|album|year|comment)\z/i
|
456
|
+
id3_fields[value.key] = value.join(', ')
|
463
457
|
end
|
464
458
|
end
|
465
459
|
@id3 = ["TAG", id3_fields['title'], id3_fields['artist'], id3_fields['album'],
|
data/test/test_apetag.rb
CHANGED
@@ -102,7 +102,6 @@ class ApeTagTest < Test::Unit::TestCase
|
|
102
102
|
assert_equal false, ai.read_only
|
103
103
|
assert_equal 'utf8', ai.ape_type
|
104
104
|
assert_equal 'BlaH', ai.key
|
105
|
-
assert_equal 'blah', ai.key_downcased
|
106
105
|
assert_equal 'BlAh', ai.string_value
|
107
106
|
assert_equal "\04\0\0\0\0\0\0\0BlaH\0BlAh", ai.raw
|
108
107
|
assert_equal true, ai.valid?
|
@@ -120,8 +119,8 @@ class ApeTagTest < Test::Unit::TestCase
|
|
120
119
|
|
121
120
|
# Test valid key settings
|
122
121
|
((("\0".."\x1f").to_a+("\x80".."\xff").to_a).collect{|x|"#{x} "} +
|
123
|
-
[nil, 1, '', 'x', 'x'*256]
|
124
|
-
("\x20".."\x7f").to_a.collect{|x|"#{x} "}+
|
122
|
+
[nil, 1, '', 'x', 'x'*256, 'id3', 'tag', 'oggs', 'mp+']).each{|x|assert_raises(ApeTagError){ai.key=x}}
|
123
|
+
("\x20".."\x7f").to_a.collect{|x|"#{x} "}+['id3', 'tag', 'oggs', 'mp+'].collect{|x|"#{x} "} +
|
125
124
|
['xx', 'x'*255].each{|x| assert_nothing_raised{ai.key=x}}
|
126
125
|
|
127
126
|
# Test valid raw and string value for different settings
|
@@ -330,11 +329,10 @@ class ApeTagTest < Test::Unit::TestCase
|
|
330
329
|
data[192] += 2
|
331
330
|
assert_raises(ApeTagError){ApeTag.new(StringIO.new(data)).fields}
|
332
331
|
|
333
|
-
# Test updating
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album']='blah'}}
|
332
|
+
# Test updating works in case insensitive manner
|
333
|
+
assert_equal ['blah'], ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['album']='blah'}['ALBUM']
|
334
|
+
assert_equal ['blah'], ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['ALBUM']='blah'}['album']
|
335
|
+
assert_equal ['blah'], ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['ALbUM']='blah'}['albuM']
|
338
336
|
|
339
337
|
# Test updating an existing ApeItem via various array methods
|
340
338
|
assert_nothing_raised{ApeTag.new(StringIO.new(EXAMPLE_APE_TAG.dup)).update{|x| x['Album'] += ['blah']}}
|
metadata
CHANGED
@@ -3,12 +3,12 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: apetag
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.0
|
7
|
-
date: 2007-
|
6
|
+
version: 1.1.0
|
7
|
+
date: 2007-08-13 00:00:00 -07:00
|
8
8
|
summary: APEv2 Tag Parser/Generator
|
9
9
|
require_paths:
|
10
10
|
- .
|
11
|
-
email:
|
11
|
+
email: code@jeremyevans.net
|
12
12
|
homepage:
|
13
13
|
rubyforge_project:
|
14
14
|
description:
|
@@ -42,5 +42,13 @@ extensions: []
|
|
42
42
|
|
43
43
|
requirements: []
|
44
44
|
|
45
|
-
dependencies:
|
46
|
-
|
45
|
+
dependencies:
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: cicphash
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.0.0
|
54
|
+
version:
|