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 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rubocop.yml ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in dmapparser.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'minitest', '~> 5.2.2'
8
+ gem 'coveralls', require: false
9
+ gem 'rubocop', '~> 0.18.0'
10
+ gem 'simplecov', require: false
11
+ end
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
+ [![Gem Version](https://badge.fury.io/rb/dmapparser.png)](http://badge.fury.io/rb/dmapparser) [![Dependency Status](https://gemnasium.com/jurriaan/dmapparser.png)](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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dmapparser'
4
+ puts DMAPParser::Parser.parse(ARGF.to_io).inspect
@@ -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,3 @@
1
+ require 'dmapparser/version'
2
+ require 'dmapparser/parser'
3
+ require 'dmapparser/builder'
@@ -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
@@ -0,0 +1,4 @@
1
+ # DMAPParser handles the parsing of DMAP data
2
+ module DMAPParser
3
+ VERSION = '0.0.1'
4
+ end
data/test/.rubocop.yml ADDED
@@ -0,0 +1,3 @@
1
+ # Do not introduce global variables.
2
+ GlobalVars:
3
+ Enabled: false
@@ -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
@@ -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: