apetag 1.0.1 → 1.1.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.
- 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:
|