dmapparser 0.0.1
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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +0 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/README.md +27 -0
- data/Rakefile +22 -0
- data/bin/dmapparser +4 -0
- data/dmapparser.gemspec +23 -0
- data/lib/dmapparser.rb +3 -0
- data/lib/dmapparser/builder.rb +49 -0
- data/lib/dmapparser/converter.rb +124 -0
- data/lib/dmapparser/parser.rb +72 -0
- data/lib/dmapparser/tag.rb +24 -0
- data/lib/dmapparser/tag_container.rb +62 -0
- data/lib/dmapparser/tag_definition.rb +32 -0
- data/lib/dmapparser/tag_definitions.rb +181 -0
- data/lib/dmapparser/version.rb +4 -0
- data/test/.rubocop.yml +3 -0
- data/test/lib/dmapparser/builder_test.rb +70 -0
- data/test/lib/dmapparser/parser_test.rb +61 -0
- data/test/lib/dmapparser/tag_container_test.rb +0 -0
- data/test/lib/dmapparser/tag_test.rb +0 -0
- data/test/support/content_codes.dmap +0 -0
- data/test/support/simple.dmap +0 -0
- data/test/test_helper.rb +23 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b4f0392efe8fbb5cb490326f252de2ba2ad14320
|
4
|
+
data.tar.gz: 83521f2817e52ffa041ae04097e41106fc23920d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4e6d311c7458783399f658271061e26e3fb7716bf9487159148f6c39d49b911ee14fc62cf676f3d91920587618d59f741bd0b84efa1b12c4da732206891243c7
|
7
|
+
data.tar.gz: 538395db0d8f903ee3f141e9bfeb53f3e4829ab313551060e6a2f291b55a3558a48a1921bbe0235b834efa24cfa41c35ee0fec2fcf2708c2781af2e9cb69bad4
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
File without changes
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Jurriaan Pruis
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# DMAPParser
|
2
|
+
|
3
|
+
[](http://badge.fury.io/rb/dmapparser) [](https://gemnasium.com/jurriaan/dmapparser)
|
4
|
+
|
5
|
+
Parses DMAP data
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'dmapparser'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install dmapparser
|
20
|
+
|
21
|
+
## Contributing
|
22
|
+
|
23
|
+
1. Fork it ( http://github.com/jurriaan/dmapparser/fork )
|
24
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
25
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
26
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
27
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rubocop'
|
4
|
+
require 'yard'
|
5
|
+
YARD::Rake::YardocTask.new
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'lib/dmapparser'
|
9
|
+
t.test_files = FileList['test/lib/**/*_test.rb']
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task test: :rubocop
|
14
|
+
|
15
|
+
task :rubocop do
|
16
|
+
puts "Running Rubocop #{Rubocop::Version::STRING}"
|
17
|
+
args = FileList['**/*.rb', 'Rakefile', 'dmapparser.gemspec', 'Gemfile']
|
18
|
+
cli = Rubocop::CLI.new
|
19
|
+
fail unless cli.run(['-a'] + args) == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
task default: :test
|
data/bin/dmapparser
ADDED
data/dmapparser.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dmapparser/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'dmapparser'
|
8
|
+
spec.version = DMAPParser::VERSION
|
9
|
+
spec.authors = ['Jurriaan Pruis']
|
10
|
+
spec.email = ['email@jurriaanpruis.nl']
|
11
|
+
spec.summary = %q{Parses DMAP data}
|
12
|
+
spec.homepage = ''
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
21
|
+
spec.add_development_dependency 'yard'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
end
|
data/lib/dmapparser.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'dmapparser/tag_definitions'
|
2
|
+
module DMAPParser
|
3
|
+
# This class provides a DSL to create DMAP responses
|
4
|
+
class Builder
|
5
|
+
attr_reader :result
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@dmap_stack = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.method_missing(method, *args, &block)
|
12
|
+
new.send(method, *args, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(method, *args, &block)
|
16
|
+
tag = TagDefinition[method]
|
17
|
+
return super if tag.nil?
|
18
|
+
|
19
|
+
if block_given?
|
20
|
+
fail "Tag #{tag} is not a container type" unless tag.container?
|
21
|
+
build_container(tag, &block)
|
22
|
+
else
|
23
|
+
build_tag(tag, args)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build_tag(tag, args)
|
30
|
+
if @dmap_stack.length > 0
|
31
|
+
args = args.size > 1 ? args : args.first
|
32
|
+
@dmap_stack.last.value << Tag.new(tag, args)
|
33
|
+
else
|
34
|
+
fail 'Cannot build DMAP without a valid container'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_container(tag , &block)
|
39
|
+
@dmap_stack << TagContainer.new(tag)
|
40
|
+
instance_exec(&block)
|
41
|
+
if @dmap_stack.length > 1
|
42
|
+
cur_tag = @dmap_stack.pop
|
43
|
+
@dmap_stack.last.value << cur_tag
|
44
|
+
else
|
45
|
+
@result = @dmap_stack.pop
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module DMAPParser
|
2
|
+
# The DMAPConverter class converts between binary and ruby formats
|
3
|
+
class Converter
|
4
|
+
class << self
|
5
|
+
def date_to_bin(data)
|
6
|
+
int_to_bin(value.to_i)
|
7
|
+
end
|
8
|
+
|
9
|
+
def bin_to_byte(data)
|
10
|
+
data.unpack('C').first
|
11
|
+
end
|
12
|
+
|
13
|
+
def bin_to_long(data)
|
14
|
+
(bin_to_int(data[0..3]) << 32) + bin_to_int(data[4..7])
|
15
|
+
end
|
16
|
+
|
17
|
+
def bin_to_int(data)
|
18
|
+
data.unpack('N').first
|
19
|
+
end
|
20
|
+
|
21
|
+
def bin_to_short(data)
|
22
|
+
data.unpack('n').first
|
23
|
+
end
|
24
|
+
|
25
|
+
def bin_to_bool(data)
|
26
|
+
data == "\x01"
|
27
|
+
end
|
28
|
+
|
29
|
+
def bin_to_version(data)
|
30
|
+
data.unpack('nCC').join '.'
|
31
|
+
end
|
32
|
+
|
33
|
+
def bin_to_hex(data)
|
34
|
+
data.bytes.reduce('') { |a, e| a + sprintf('%02X', e) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def bin_to_date(data)
|
38
|
+
Time.at(bin_to_int(data))
|
39
|
+
end
|
40
|
+
|
41
|
+
def bool_to_bin(data)
|
42
|
+
(data ? 1 : 0).chr
|
43
|
+
end
|
44
|
+
|
45
|
+
def int_to_bin(data)
|
46
|
+
[data.to_i].pack 'N'
|
47
|
+
end
|
48
|
+
|
49
|
+
def byte_to_bin(data)
|
50
|
+
[data.to_i].pack 'C'
|
51
|
+
end
|
52
|
+
|
53
|
+
def long_to_bin(data)
|
54
|
+
[data >> 32, data & 0xfffffff].pack 'NN'
|
55
|
+
end
|
56
|
+
|
57
|
+
def short_to_bin(data)
|
58
|
+
[data.to_i].pack 'n'
|
59
|
+
end
|
60
|
+
|
61
|
+
def version_to_bin(data)
|
62
|
+
data.split('.').pack 'nCC'
|
63
|
+
end
|
64
|
+
|
65
|
+
def hex_to_bin(data)
|
66
|
+
[data].pack 'H*'
|
67
|
+
end
|
68
|
+
|
69
|
+
def decode_unknown(data)
|
70
|
+
if data =~ /[^\x20-\x7e]/ # non-readable characters
|
71
|
+
data_to_numeric(data)
|
72
|
+
else
|
73
|
+
data
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def data_to_numeric(data)
|
78
|
+
if data.bytesize == 1
|
79
|
+
return Converter.bin_to_byte(data)
|
80
|
+
elsif data.bytesize == 2
|
81
|
+
return Converter.bin_to_short(data)
|
82
|
+
elsif data.bytesize == 4
|
83
|
+
return Converter.bin_to_int(data)
|
84
|
+
elsif data.bytesize == 8
|
85
|
+
return Converter.bin_to_long(data)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def bin_to_string(data)
|
90
|
+
data.force_encoding('utf-8')
|
91
|
+
end
|
92
|
+
alias_method :string_to_bin, :bin_to_string
|
93
|
+
|
94
|
+
alias_method :uint16_to_bin, :short_to_bin
|
95
|
+
alias_method :uint32_to_bin, :int_to_bin
|
96
|
+
alias_method :uint64_to_bin, :long_to_bin
|
97
|
+
|
98
|
+
alias_method :bin_to_uint16, :bin_to_short
|
99
|
+
alias_method :bin_to_uint32, :bin_to_int
|
100
|
+
alias_method :bin_to_uint64, :bin_to_long
|
101
|
+
alias_method :bin_to_unknown, :decode_unknown
|
102
|
+
|
103
|
+
def decode(type, data)
|
104
|
+
decode_method = ('bin_to_' + type.to_s).to_sym
|
105
|
+
if respond_to? decode_method
|
106
|
+
send(decode_method, data)
|
107
|
+
else
|
108
|
+
warn "Decoder: Unknown type #{type}"
|
109
|
+
decode_unknown(data)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def encode(type, data)
|
114
|
+
encode_method = (type.to_s + '_to_bin').to_sym
|
115
|
+
if respond_to? encode_method
|
116
|
+
send(encode_method, data)
|
117
|
+
else
|
118
|
+
warn "Encoder: Unknown type #{type}"
|
119
|
+
data
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'dmapparser/tag_definitions'
|
2
|
+
require 'dmapparser/converter'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module DMAPParser
|
6
|
+
# The DMAPParser class parses DMAP responses
|
7
|
+
class Parser
|
8
|
+
ParseError = Class.new(StandardError)
|
9
|
+
|
10
|
+
def self.parse(response)
|
11
|
+
new(response).parse
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(response)
|
15
|
+
@response = response
|
16
|
+
@response = StringIO.new(response) unless @response.is_a? IO
|
17
|
+
@response.set_encoding(Encoding::UTF_8) # Use unicode
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse
|
21
|
+
return nil if @response.nil? || @response.size == 0
|
22
|
+
fail ParseError if @response.size < 8
|
23
|
+
ret = TagContainer.new
|
24
|
+
ret.type = TagDefinition[read_key]
|
25
|
+
fail ParseError if ret.type && !ret.type.container?
|
26
|
+
ret.value = parse_container(read_length)
|
27
|
+
ret
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def bytes_left
|
33
|
+
@response.size - @response.pos
|
34
|
+
end
|
35
|
+
|
36
|
+
def bytes_available?(num = 1)
|
37
|
+
bytes_left - num >= 0
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_bytes(length)
|
41
|
+
unless bytes_available?(length)
|
42
|
+
fail ParseError, 'Not enough data available'
|
43
|
+
end
|
44
|
+
@response.read(length)
|
45
|
+
end
|
46
|
+
|
47
|
+
def read_length
|
48
|
+
Converter.bin_to_int(read_bytes(4))
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_key
|
52
|
+
read_bytes(4)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_container(container_length)
|
56
|
+
values = []
|
57
|
+
end_pos = container_length + @response.pos
|
58
|
+
values << build_tag(read_key, read_length) until @response.pos == end_pos
|
59
|
+
values
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_tag(key, length)
|
63
|
+
tag = TagDefinition[key] ||
|
64
|
+
TagDefinition.new(key, :unknown, "unknown (#{length})")
|
65
|
+
if tag.container?
|
66
|
+
TagContainer.new(tag, parse_container(length))
|
67
|
+
else
|
68
|
+
Tag.new(tag, Converter.decode(tag.type, read_bytes(length)))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DMAPParser
|
2
|
+
# The Tag class
|
3
|
+
class Tag < Struct.new(:type, :value)
|
4
|
+
def to_s
|
5
|
+
"#<#{self.class.name} #{type}>"
|
6
|
+
end
|
7
|
+
|
8
|
+
def inspect(level = 0)
|
9
|
+
"#{' ' * level}#{type}: #{value}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_dmap
|
13
|
+
value = convert_value(self.value)
|
14
|
+
length = [value.length].pack('N')
|
15
|
+
(type.tag.to_s + length + value).force_encoding('utf-8')
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def convert_value(value)
|
21
|
+
Converter.encode(type.type, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module DMAPParser
|
2
|
+
# The TagContainer class is a Tag which contains other Tags
|
3
|
+
class TagContainer < Tag
|
4
|
+
def initialize(type = nil, value = [])
|
5
|
+
super type, value
|
6
|
+
end
|
7
|
+
|
8
|
+
def inspect(level = 0)
|
9
|
+
"#{' ' * level}#{type}:\n" + value.reduce('') do |a, e|
|
10
|
+
a + e.inspect(level + 1).chomp + "\n"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_value(key)
|
15
|
+
return value[key] if key.is_a? Fixnum
|
16
|
+
|
17
|
+
tag = get_tag(key)
|
18
|
+
|
19
|
+
if tag.type.container?
|
20
|
+
tag
|
21
|
+
elsif !tag.nil?
|
22
|
+
tag.value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_tag(key)
|
27
|
+
key = key.to_s
|
28
|
+
value.find { |e| e.type.tag == key || e.type.name == key }
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :[], :get_value
|
32
|
+
|
33
|
+
def has?(key)
|
34
|
+
!get_tag(key).nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(method, *arguments, &block)
|
38
|
+
if method =~ /(.*)\?$/
|
39
|
+
has?(Regexp.last_match[1])
|
40
|
+
elsif has?(method)
|
41
|
+
get_value(method)
|
42
|
+
else
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def respond_to?(method)
|
48
|
+
method.to_s.match(/(.*)\??$/)
|
49
|
+
has?(Regexp.last_match[1]) || super
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_a
|
53
|
+
value
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def convert_value(value)
|
59
|
+
value.reduce('') { |a, e| a + e.to_dmap }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'dmapparser/tag'
|
2
|
+
require 'dmapparser/tag_container'
|
3
|
+
|
4
|
+
module DMAPParser
|
5
|
+
# The TagDefinition class describes the tags
|
6
|
+
class TagDefinition < Struct.new(:tag, :type, :name)
|
7
|
+
DEFINITIONS = Hash.new(nil)
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
"#{tag} (#{name}: #{type})"
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :inspect, :to_s
|
14
|
+
|
15
|
+
def container?
|
16
|
+
type == :container
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def find(key)
|
21
|
+
DEFINITIONS[key.to_s]
|
22
|
+
end
|
23
|
+
|
24
|
+
def tag(*args, &block)
|
25
|
+
definition = new(*args, &block).freeze
|
26
|
+
DEFINITIONS[definition.tag.to_s] = definition
|
27
|
+
end
|
28
|
+
|
29
|
+
alias_method :[], :find
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'dmapparser/tag_definition'
|
2
|
+
module DMAPParser
|
3
|
+
# Sources:
|
4
|
+
# https://github.com/chendo/dmap-ng/blob/master/lib/dmap/tag_definitions.rb
|
5
|
+
# https://code.google.com/p/ytrack/wiki/DMAP
|
6
|
+
# https://code.google.com/p/tunesremote-se/wiki/ContentCodes
|
7
|
+
# https://raw.github.com/mattstevens/dmap-parser/master/dmap_parser.c
|
8
|
+
# /content-codes
|
9
|
+
# rubocop:disable ClassLength
|
10
|
+
class TagDefinition
|
11
|
+
tag 'mcon', :container, 'dmap.container'
|
12
|
+
tag 'msrv', :container, 'dmap.serverinforesponse'
|
13
|
+
tag 'msml', :container, 'dmap.msml'
|
14
|
+
tag 'mccr', :container, 'dmap.contentcodesresponse'
|
15
|
+
tag 'mdcl', :container, 'dmap.dictionary'
|
16
|
+
tag 'mlog', :container, 'dmap.loginresponse'
|
17
|
+
tag 'mupd', :container, 'dmap.updateresponse'
|
18
|
+
tag 'avdb', :container, 'daap.serverdatabases'
|
19
|
+
tag 'mlcl', :container, 'dmap.listing'
|
20
|
+
tag 'mlit', :container, 'dmap.listingitem'
|
21
|
+
tag 'mbcl', :container, 'dmap.bag'
|
22
|
+
tag 'adbs', :container, 'daap.returndatabasesongs'
|
23
|
+
tag 'aply', :container, 'daap.databaseplaylists'
|
24
|
+
tag 'apso', :container, 'daap.playlistsongs'
|
25
|
+
tag 'mudl', :container, 'dmap.deletedidlisting'
|
26
|
+
tag 'abro', :container, 'daap.databasebrowse'
|
27
|
+
tag 'abal', :container, 'daap.browsealbumlisting'
|
28
|
+
tag 'abar', :container, 'daap.browseartistlisting'
|
29
|
+
tag 'abcp', :container, 'daap.browsecomposerlisting'
|
30
|
+
tag 'abgn', :container, 'daap.browsegenrelisting'
|
31
|
+
tag 'prsv', :container, 'daap.resolve'
|
32
|
+
tag 'arif', :container, 'daap.resolveinfo'
|
33
|
+
tag 'casp', :container, 'dacp.speakers'
|
34
|
+
tag 'caci', :container, 'dacp.controlint'
|
35
|
+
tag 'cmpa', :container, 'dacp.pairinganswer'
|
36
|
+
tag 'cacr', :container, 'dacp.cacr'
|
37
|
+
tag 'cmcp', :container, 'dmcp.controlprompt'
|
38
|
+
tag 'cmgt', :container, 'dmcp.getpropertyresponse'
|
39
|
+
tag 'cmst', :container, 'dmcp.status'
|
40
|
+
tag 'agal', :container, 'daap.albumgrouping'
|
41
|
+
tag 'minm', :string, 'dmap.itemname'
|
42
|
+
tag 'msts', :string, 'dmap.statusstring'
|
43
|
+
tag 'mcna', :string, 'dmap.contentcodesname'
|
44
|
+
tag 'asal', :string, 'daap.songalbum'
|
45
|
+
tag 'asaa', :string, 'daap.songalbumartist'
|
46
|
+
tag 'asar', :string, 'daap.songartist'
|
47
|
+
tag 'ascm', :string, 'daap.songcomment'
|
48
|
+
tag 'asfm', :string, 'daap.songformat'
|
49
|
+
tag 'aseq', :string, 'daap.songeqpreset'
|
50
|
+
tag 'asgn', :string, 'daap.songgenre'
|
51
|
+
tag 'asdt', :string, 'daap.songdescription'
|
52
|
+
tag 'asul', :string, 'daap.songdataurl'
|
53
|
+
tag 'ceWM', :string, 'com.apple.itunes.welcome-message'
|
54
|
+
tag 'ascp', :string, 'daap.songcomposer'
|
55
|
+
tag 'assu', :string, 'daap.sortartist'
|
56
|
+
tag 'assa', :string, 'daap.sortalbum'
|
57
|
+
tag 'agrp', :string, 'daap.songgrouping'
|
58
|
+
tag 'cann', :string, 'daap.nowplayingtrack'
|
59
|
+
tag 'cana', :string, 'daap.nowplayingartist'
|
60
|
+
tag 'canl', :string, 'daap.nowplayingalbum'
|
61
|
+
tag 'cang', :string, 'daap.nowplayinggenre'
|
62
|
+
tag 'cmnm', :string, 'dacp.devicename'
|
63
|
+
tag 'cmty', :string, 'dacp.devicetype'
|
64
|
+
tag 'cmpg', :hex, 'dacp.pairingguid' # hex string
|
65
|
+
tag 'mper', :uint64, 'dmap.persistentid'
|
66
|
+
tag 'canp', :uint64, 'dacp.nowplaying'
|
67
|
+
tag 'cmpy', :uint64, 'dacp.passguid'
|
68
|
+
tag 'mdbk', :uint32, 'dmap.databasekind'
|
69
|
+
tag 'aeCs', :uint32, 'com.apple.itunes.artworkchecksum'
|
70
|
+
tag 'mctc', :uint32, 'dmap.containercount'
|
71
|
+
tag 'aeMk', :uint32, 'com.apple.itunes.mediakind'
|
72
|
+
tag 'aeMK', :uint32, 'com.apple.itunes.extended-mediakind'
|
73
|
+
tag 'mstt', :uint32, 'dmap.status' # http status??
|
74
|
+
tag 'mcnm', :string, 'dmap.contentcodesnumber'
|
75
|
+
tag 'meds', :uint32, 'dmap.editcommandssupported'
|
76
|
+
tag 'miid', :uint32, 'dmap.itemid'
|
77
|
+
tag 'mcti', :uint32, 'dmap.containeritemid'
|
78
|
+
tag 'mpco', :uint32, 'dmap.parentcontainerid'
|
79
|
+
tag 'mimc', :uint32, 'dmap.itemcount'
|
80
|
+
tag 'mrco', :uint32, 'dmap.returnedcount'
|
81
|
+
tag 'mtco', :uint32, 'dmap.containercount'
|
82
|
+
tag 'mstm', :uint32, 'dmap.timeoutinterval'
|
83
|
+
tag 'msdc', :uint32, 'dmap.databasescount'
|
84
|
+
tag 'msma', :uint32, 'dmap.speakermachineaddress'
|
85
|
+
tag 'mlid', :uint32, 'dmap.sessionid'
|
86
|
+
tag 'assr', :uint32, 'daap.songsamplerate'
|
87
|
+
tag 'assz', :uint32, 'daap.songsize'
|
88
|
+
tag 'asst', :uint32, 'daap.songstarttime'
|
89
|
+
tag 'assp', :uint32, 'daap.songstoptime'
|
90
|
+
tag 'astm', :uint32, 'daap.songtime'
|
91
|
+
tag 'msto', :uint32, 'dmap.utcoffset'
|
92
|
+
tag 'cmsr', :uint32, 'dmcp.mediarevision'
|
93
|
+
tag 'caas', :uint32, 'dacp.albumshuffle'
|
94
|
+
tag 'caar', :uint32, 'dacp.albumrepeat'
|
95
|
+
tag 'cant', :uint32, 'dacp.remainingtime'
|
96
|
+
tag 'cmmk', :uint32, 'dmcp.mediakind'
|
97
|
+
tag 'cast', :uint32, 'dacp.tracklength'
|
98
|
+
tag 'asai', :uint32, 'daap.songalbumid'
|
99
|
+
tag 'aeNV', :uint32, 'com.apple.itunes.norm-volume'
|
100
|
+
tag 'cmvo', :uint32, 'dmcp.volume'
|
101
|
+
tag 'mcty', :uint16, 'dmap.contentcodestype'
|
102
|
+
tag 'asbt', :uint16, 'daap.songsbeatsperminute'
|
103
|
+
tag 'asbr', :uint16, 'daap.songbitrate'
|
104
|
+
tag 'asdc', :uint16, 'daap.songdisccount'
|
105
|
+
tag 'asdn', :uint16, 'daap.songdiscnumber'
|
106
|
+
tag 'astc', :uint16, 'daap.songtrackcount'
|
107
|
+
tag 'astn', :uint16, 'daap.songtracknumber'
|
108
|
+
tag 'asyr', :uint16, 'daap.songyear'
|
109
|
+
tag 'ated', :uint16, 'daap.supportsextradata'
|
110
|
+
tag 'asgr', :uint16, 'daap.supportsgroups'
|
111
|
+
tag 'mikd', :byte, 'dmap.itemkind'
|
112
|
+
tag 'casu', :byte, 'dacp.su'
|
113
|
+
tag 'msau', :byte, 'dmap.authenticationmethod'
|
114
|
+
tag 'mstu', :byte, 'dmap.updatetype'
|
115
|
+
tag 'asrv', :byte, 'daap.songrelativevolume'
|
116
|
+
tag 'asur', :byte, 'daap.songuserrating'
|
117
|
+
tag 'asdk', :byte, 'daap.songdatakind'
|
118
|
+
tag 'caps', :byte, 'dacp.playstatus'
|
119
|
+
tag 'cash', :byte, 'dacp.shufflestate'
|
120
|
+
tag 'carp', :byte, 'dacp.repeatstate'
|
121
|
+
tag 'cavc', :byte, 'dacp.volumecontrollable'
|
122
|
+
tag 'cafe', :byte, 'dacp.fullscreenenabled'
|
123
|
+
tag 'cave', :byte, 'dacp.visualizerenabled'
|
124
|
+
tag 'muty', :byte, 'dmap.updatetype'
|
125
|
+
tag "f\215ch", :byte, 'dmap.haschildcontainers'
|
126
|
+
tag 'msas', :byte, 'dmap.authenticationschemes'
|
127
|
+
tag 'cavs', :bool, 'dacp.visualizer'
|
128
|
+
tag 'cafs', :bool, 'dacp.fullscreen'
|
129
|
+
tag 'ceGS', :bool, 'com.apple.itunes.genius-selectable'
|
130
|
+
tag 'mslr', :bool, 'dmap.loginrequired'
|
131
|
+
tag 'msal', :bool, 'dmap.supportsautologout'
|
132
|
+
tag 'msup', :bool, 'dmap.supportsupdate'
|
133
|
+
tag 'mspi', :bool, 'dmap.supportspersistenids'
|
134
|
+
tag 'msex', :bool, 'dmap.supportsextensions'
|
135
|
+
tag 'msbr', :bool, 'dmap.supportsbrowse'
|
136
|
+
tag 'msqy', :bool, 'dmap.supportsquery'
|
137
|
+
tag 'msix', :bool, 'dmap.supportsindex'
|
138
|
+
tag 'msrs', :bool, 'dmap.supportsresolve'
|
139
|
+
tag 'asco', :bool, 'daap.songcompliation'
|
140
|
+
tag 'asdb', :bool, 'daap.songdisabled'
|
141
|
+
tag 'abpl', :bool, 'daap.baseplaylist'
|
142
|
+
tag 'aeSP', :bool, 'com.apple.itunes.smart-playlist'
|
143
|
+
tag 'aePP', :bool, 'com.apple.itunes.is-podcast-playlist'
|
144
|
+
tag 'aePS', :bool, 'com.apple.itunes.special-playlist'
|
145
|
+
tag 'aeSG', :bool, 'com.apple.itunes.saved-genius'
|
146
|
+
tag 'aeFP', :bool, 'com.apple.itunes.req-fplay'
|
147
|
+
tag 'aeHV', :bool, 'com.apple.itunes.has-video'
|
148
|
+
tag 'caia', :bool, 'dacp.isavailiable'
|
149
|
+
tag 'caip', :bool, 'dacp.isplaying'
|
150
|
+
tag 'ceVO', :bool, 'com.apple.itunes.voting-enabled'
|
151
|
+
tag 'aeSV', :version, 'com.apple.itunes.music-sharing-version'
|
152
|
+
tag 'mpro', :version, 'dmap.protocolversion'
|
153
|
+
tag 'apro', :version, 'daap.protocolversion'
|
154
|
+
tag 'musr', :version, 'dmap.serverrevision'
|
155
|
+
tag 'mstc', :date, 'dmap.utc-time'
|
156
|
+
tag 'asda', :date, 'daap.songdateadded'
|
157
|
+
tag 'asdm', :date, 'daap.songdatemodified'
|
158
|
+
tag 'ceJC', :bool, 'com.apple.itunes.jukebox-client-vote'
|
159
|
+
tag 'ceJI', :bool, 'com.apple.itunes.jukebox-current'
|
160
|
+
tag 'ceJS', :uint16, 'com.apple.itunes.jukebox-score'
|
161
|
+
tag 'ceJV', :bool, 'com.apple.itunes.jukebox-vote'
|
162
|
+
tag 'ceQR', :container, 'com.apple.itunes.playqueue-contents-response'
|
163
|
+
tag 'ceQS', :container, 'com.apple.itunes.playqueue-contents-???'
|
164
|
+
tag 'ceQs', :uint64, 'com.apple.itunes.playqueue-id'
|
165
|
+
tag 'ceQa', :string, 'com.apple.itunes.playqueue-album'
|
166
|
+
tag 'ceQg', :string, 'com.apple.itunes.playqueue-genre'
|
167
|
+
tag 'ceQn', :string, 'com.apple.itunes.playqueue-name'
|
168
|
+
tag 'ceQr', :string, 'com.apple.itunes.playqueue-artist'
|
169
|
+
tag 'ceQk', :string, 'com.apple.itunes.playqueue-key'
|
170
|
+
tag 'ceQl', :string, 'com.apple.itunes.playqueue-label'
|
171
|
+
tag 'msml', :container, 'msml'
|
172
|
+
tag 'aeGs', :bool, 'com.apple.itunes.can-be-genius-seed'
|
173
|
+
tag 'aprm', :short, 'daap.playlistrepeatmode'
|
174
|
+
tag 'apsm', :short, 'daap.playlistshufflemode'
|
175
|
+
tag 'cmpr', :version, 'dmcp.protocolversion'
|
176
|
+
tag 'capr', :version, 'dacp.protocolversion'
|
177
|
+
tag 'ppro', :version, 'unknown.version'
|
178
|
+
tag 'aeXD', :string, 'com.apple.itunes.xid'
|
179
|
+
end
|
180
|
+
# rubocop:enable ClassLength
|
181
|
+
end
|
data/test/.rubocop.yml
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
describe DMAPParser::Builder do
|
4
|
+
before do
|
5
|
+
@pair_data = DMAPParser::Builder.cmpa do
|
6
|
+
cmpg 'AABBCCDDEE'
|
7
|
+
cmnm 'Name'
|
8
|
+
cmty 'iPod'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should return a valid string' do
|
13
|
+
dmap = @pair_data.to_dmap
|
14
|
+
dmap.must_be_instance_of String
|
15
|
+
DMAPParser::Parser.parse(dmap).to_dmap.must_equal(dmap)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should raise an exception if tags are put inside non-containers' do
|
19
|
+
lambda do
|
20
|
+
DMAPParser::Builder.cmnm do
|
21
|
+
cmpg 'AABBCCDDEE'
|
22
|
+
cmty 'iPod'
|
23
|
+
end
|
24
|
+
end.must_raise RuntimeError
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should return a TagContainer' do
|
28
|
+
@pair_data.must_be_instance_of DMAPParser::TagContainer
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should store the correct values' do
|
32
|
+
@pair_data.cmpg.must_equal 'AABBCCDDEE'
|
33
|
+
@pair_data.cmnm.must_equal 'Name'
|
34
|
+
@pair_data.cmty.must_equal 'iPod'
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should store the correct Tags' do
|
38
|
+
@pair_data.get_tag(:cmpg).must_be_instance_of DMAPParser::Tag
|
39
|
+
@pair_data.get_tag(:cmnm).must_be_instance_of DMAPParser::Tag
|
40
|
+
@pair_data.get_tag(:cmty).must_be_instance_of DMAPParser::Tag
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should fail if there is no container' do
|
44
|
+
-> { DMAPParser::Builder.cmnm }.must_raise RuntimeError
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should support deep nesting' do
|
48
|
+
nested = DMAPParser::Builder.mcon do
|
49
|
+
miid 0
|
50
|
+
mcon do
|
51
|
+
miid 1
|
52
|
+
mcon do
|
53
|
+
miid 2
|
54
|
+
mcon do
|
55
|
+
miid 3
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
nested.must_be_instance_of DMAPParser::TagContainer
|
62
|
+
mcon = nested
|
63
|
+
3.times do |i|
|
64
|
+
mcon.miid.must_equal i
|
65
|
+
mcon = mcon.mcon
|
66
|
+
end
|
67
|
+
dmap = nested.to_dmap
|
68
|
+
DMAPParser::Parser.parse(dmap).to_dmap.must_equal(dmap)
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../../test_helper'
|
2
|
+
|
3
|
+
describe DMAPParser::Parser do
|
4
|
+
it 'should raise a ParserError when given invalid data' do
|
5
|
+
invalid = []
|
6
|
+
invalid << (0..255).map { |c| c.chr }.join # random data
|
7
|
+
invalid << "rand\x00\x00\x00\x01" # non correct size
|
8
|
+
invalid << "msup\x00\x00\x00\x01\x01" # not a container!
|
9
|
+
invalid << "rand\x00\x00\x00\x01\x01" # not a container!
|
10
|
+
invalid.each do |data|
|
11
|
+
lambda do
|
12
|
+
DMAPParser::Parser.parse(data)
|
13
|
+
end.must_raise(DMAPParser::Parser::ParseError)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return nil if the string is empty' do
|
18
|
+
DMAPParser::Parser.parse('').must_equal nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should ignore padding' do
|
22
|
+
dmap = support_file('simple.dmap').read
|
23
|
+
padded = DMAPParser::Parser.parse(dmap + 'I AM PADDING!!11!!')
|
24
|
+
padded.to_dmap.must_equal dmap
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should parse an empty container' do
|
28
|
+
data = "mcon\x00\x00\x00\x00"
|
29
|
+
mcon = DMAPParser::Parser.parse(data)
|
30
|
+
mcon.must_be_instance_of DMAPParser::TagContainer
|
31
|
+
mcon.to_a.must_be_empty
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should accept IO objects' do
|
35
|
+
data = DMAPParser::Parser.parse(support_file('simple.dmap'))
|
36
|
+
data.mstt.must_equal 200
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should parse iTunes content-types' do
|
40
|
+
data = DMAPParser::Parser.parse(support_file('content_codes.dmap'))
|
41
|
+
data.type.tag.must_equal 'mccr'
|
42
|
+
data.mstt.must_equal 200
|
43
|
+
data.to_a.count.must_equal 184
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should parse unknown tags' do
|
47
|
+
data = "mcon\x00\x00\x00\x15xkcd\x00\x00\x00\x0dis a webcomic"
|
48
|
+
data = DMAPParser::Parser.parse(data)
|
49
|
+
data.xkcd?.must_equal true
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should guess the type of unknown tags' do
|
53
|
+
data = "mcon\x00\x00\x00\x15xkcd\x00\x00\x00\x0Dis a webcomic"
|
54
|
+
data = DMAPParser::Parser.parse(data)
|
55
|
+
data.xkcd.must_equal 'is a webcomic'
|
56
|
+
data = "mcon\x00\x00\x00\x0Ajurp\x00\x00\x00\x02\x13\x37"
|
57
|
+
data = DMAPParser::Parser.parse(data)
|
58
|
+
data.jurp.must_equal 0x1337
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
File without changes
|
File without changes
|
Binary file
|
Binary file
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'coveralls'
|
3
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
4
|
+
SimpleCov::Formatter::HTMLFormatter,
|
5
|
+
Coveralls::SimpleCov::Formatter
|
6
|
+
]
|
7
|
+
SimpleCov.start do
|
8
|
+
# add_filter '/test/'
|
9
|
+
end
|
10
|
+
gem 'minitest'
|
11
|
+
require 'minitest/autorun'
|
12
|
+
require 'minitest/hell'
|
13
|
+
require 'minitest/pride'
|
14
|
+
|
15
|
+
$TEST_DIR = __dir__
|
16
|
+
$ROOT_DIR = File.expand_path('..', $TEST_DIR)
|
17
|
+
$SUPPORT_DIR = File.join(__dir__, 'support/')
|
18
|
+
$LOAD_PATH.unshift File.join($ROOT_DIR, 'lib/')
|
19
|
+
|
20
|
+
require File.expand_path('../lib/dmapparser.rb', __dir__)
|
21
|
+
def support_file(name)
|
22
|
+
open(File.join($SUPPORT_DIR, name))
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dmapparser
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jurriaan Pruis
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: yard
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- email@jurriaanpruis.nl
|
58
|
+
executables:
|
59
|
+
- dmapparser
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- ".rubocop.yml"
|
65
|
+
- Gemfile
|
66
|
+
- LICENSE
|
67
|
+
- README.md
|
68
|
+
- Rakefile
|
69
|
+
- bin/dmapparser
|
70
|
+
- dmapparser.gemspec
|
71
|
+
- lib/dmapparser.rb
|
72
|
+
- lib/dmapparser/builder.rb
|
73
|
+
- lib/dmapparser/converter.rb
|
74
|
+
- lib/dmapparser/parser.rb
|
75
|
+
- lib/dmapparser/tag.rb
|
76
|
+
- lib/dmapparser/tag_container.rb
|
77
|
+
- lib/dmapparser/tag_definition.rb
|
78
|
+
- lib/dmapparser/tag_definitions.rb
|
79
|
+
- lib/dmapparser/version.rb
|
80
|
+
- test/.rubocop.yml
|
81
|
+
- test/lib/dmapparser/builder_test.rb
|
82
|
+
- test/lib/dmapparser/parser_test.rb
|
83
|
+
- test/lib/dmapparser/tag_container_test.rb
|
84
|
+
- test/lib/dmapparser/tag_test.rb
|
85
|
+
- test/support/content_codes.dmap
|
86
|
+
- test/support/simple.dmap
|
87
|
+
- test/test_helper.rb
|
88
|
+
homepage: ''
|
89
|
+
licenses:
|
90
|
+
- MIT
|
91
|
+
metadata: {}
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
requirements: []
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 2.2.0
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Parses DMAP data
|
112
|
+
test_files:
|
113
|
+
- test/.rubocop.yml
|
114
|
+
- test/lib/dmapparser/builder_test.rb
|
115
|
+
- test/lib/dmapparser/parser_test.rb
|
116
|
+
- test/lib/dmapparser/tag_container_test.rb
|
117
|
+
- test/lib/dmapparser/tag_test.rb
|
118
|
+
- test/support/content_codes.dmap
|
119
|
+
- test/support/simple.dmap
|
120
|
+
- test/test_helper.rb
|
121
|
+
has_rdoc:
|