dacpclient 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -1
- data/Gemfile +0 -9
- data/README.md +24 -0
- data/Rakefile +1 -1
- data/bin/dacpclient +61 -6
- data/dacpclient.gemspec +6 -1
- data/lib/dacpclient/bonjour.rb +43 -0
- data/lib/dacpclient/client.rb +112 -53
- data/lib/dacpclient/dmapbuilder.rb +20 -18
- data/lib/dacpclient/dmapconverter.rb +60 -7
- data/lib/dacpclient/dmapparser.rb +29 -75
- data/lib/dacpclient/pairingserver.rb +33 -34
- data/lib/dacpclient/tag.rb +21 -0
- data/lib/dacpclient/tag_container.rb +49 -0
- data/lib/dacpclient/tag_definition.rb +29 -0
- data/lib/dacpclient/tag_definitions.rb +167 -0
- data/lib/dacpclient/version.rb +2 -2
- data/lib/dacpclient.rb +1 -1
- metadata +79 -5
- data/lib/dacpclient/tagdefinitions.rb +0 -264
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef5304a1a170b2d08364485ca254ff1f2607fba3
|
4
|
+
data.tar.gz: f7369e5cc191eae00af1a9b02e4e276069385496
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6c8b74aac51cfa281f8201e52b49044ec1009bfca8fed4252c5d2da9dc9abeaba0bf3e755605900f30f1f521885d54c9e4614741fcaee4dffdcbc73b4165331e
|
7
|
+
data.tar.gz: e4204cd25fd0223c49b59dddfc032f91714827478ba3c6ca6692037b0ba83e15c771ea03aea0d96546704f34889230a44ef583b244d727f49d1426ba7d4094be
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
A DACP (iTunes Remote protocol) client written in the wonderful Ruby language.
|
6
6
|
You can use this for controlling iTunes. It uses the same protocol as the iTunes remote iOS app.
|
7
7
|
|
8
|
+
You can control iTunes by connecting and entering a pin, or with Home Sharing. DACPClient supports both methods.
|
9
|
+
|
8
10
|
Look at the [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient) file for an example client.
|
9
11
|
|
10
12
|
## Installation
|
@@ -25,8 +27,30 @@ Or install it yourself using:
|
|
25
27
|
|
26
28
|
See [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient)
|
27
29
|
|
30
|
+
Usage: dacpclient [command]
|
31
|
+
(c) 2013 Jurriaan Pruis <email@jurriaanpruis.nl>
|
32
|
+
|
33
|
+
Where command is one of the following:
|
34
|
+
status
|
35
|
+
status_ticker
|
36
|
+
home_sharing
|
37
|
+
play
|
38
|
+
pause
|
39
|
+
playpause
|
40
|
+
next
|
41
|
+
prev
|
42
|
+
databases
|
43
|
+
playqueue
|
44
|
+
upnext
|
45
|
+
stop
|
46
|
+
debug
|
47
|
+
usage
|
48
|
+
previous
|
49
|
+
help
|
50
|
+
|
28
51
|
## Todo
|
29
52
|
|
53
|
+
- Use bonjour
|
30
54
|
- Add tests
|
31
55
|
- Add more tagdefinitions
|
32
56
|
- Documentation
|
data/Rakefile
CHANGED
data/bin/dacpclient
CHANGED
@@ -1,13 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
|
3
2
|
require 'dacpclient'
|
4
|
-
require '
|
3
|
+
require 'English'
|
5
4
|
require 'socket'
|
5
|
+
require 'yaml'
|
6
|
+
require 'yaml/dbm'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'io/console'
|
6
9
|
|
7
10
|
# This is the CLI DACP Client. Normally installed as `dacpclient`
|
8
11
|
class CLIClient
|
9
12
|
def initialize
|
10
|
-
@
|
13
|
+
@config = {}
|
14
|
+
@config['client_name'] ||= "DACPClient (#{Socket.gethostname})"
|
15
|
+
load_config
|
16
|
+
|
17
|
+
@client = DACPClient::Client.new(@config['client_name'], 'localhost', 3689)
|
18
|
+
if @config['guid'].nil? || @config['guid'] !~ /^[a-f0-9]{16}$/
|
19
|
+
@config['guid'] = @client.get_guid
|
20
|
+
save_config
|
21
|
+
end
|
22
|
+
@client.guid = @config['guid']
|
11
23
|
@login = false
|
12
24
|
end
|
13
25
|
|
@@ -64,6 +76,23 @@ class CLIClient
|
|
64
76
|
end
|
65
77
|
end
|
66
78
|
|
79
|
+
def home_sharing
|
80
|
+
puts "Setting up Home Sharing. Saving Home Sharing GUID to ~/.dacpclient/config.yml"
|
81
|
+
puts "\nPlease enter your Apple ID credentials:"
|
82
|
+
print "Apple ID (e-mail address): "
|
83
|
+
email = $stdin.gets.strip
|
84
|
+
print "Password: "
|
85
|
+
password = $stdin.noecho(&:gets).chomp
|
86
|
+
guid = @client.setup_home_sharing(email, password)
|
87
|
+
password = nil
|
88
|
+
@config['appleid'] = email
|
89
|
+
@config['hsgid'] = guid
|
90
|
+
save_config
|
91
|
+
puts "\n\n"
|
92
|
+
puts "Got your Home Sharing GUID (#{guid}). Logging in.."
|
93
|
+
login
|
94
|
+
end
|
95
|
+
|
67
96
|
def play
|
68
97
|
login
|
69
98
|
@client.play
|
@@ -101,7 +130,7 @@ class CLIClient
|
|
101
130
|
|
102
131
|
def upnext
|
103
132
|
login
|
104
|
-
items = @client.list_queue.mlcl.select {
|
133
|
+
items = @client.list_queue.mlcl.to_a.select {|i| i.type.tag == 'mlit' }
|
105
134
|
puts 'Up next:'
|
106
135
|
puts '--------'
|
107
136
|
puts
|
@@ -140,7 +169,13 @@ class CLIClient
|
|
140
169
|
private
|
141
170
|
|
142
171
|
def login
|
143
|
-
|
172
|
+
return if @login
|
173
|
+
@client.hsgid = @config['hsgid']
|
174
|
+
if @client.hsgid.nil?
|
175
|
+
@client.pair_and_login
|
176
|
+
else
|
177
|
+
@client.login
|
178
|
+
end
|
144
179
|
@login = true
|
145
180
|
end
|
146
181
|
|
@@ -165,7 +200,27 @@ class CLIClient
|
|
165
200
|
end
|
166
201
|
end
|
167
202
|
end
|
203
|
+
|
204
|
+
def config_dir
|
205
|
+
File.join(ENV['HOME'], '.dacpclient')
|
206
|
+
end
|
207
|
+
|
208
|
+
def load_config
|
209
|
+
FileUtils.mkdir_p(config_dir)
|
210
|
+
config_file = File.join(config_dir,'config.yml')
|
211
|
+
if File.exists? config_file
|
212
|
+
@config.merge! YAML.load_file(config_file)
|
213
|
+
else
|
214
|
+
save_config
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def save_config
|
219
|
+
File.open(File.join(config_dir,'config.yml'), 'w') do |out|
|
220
|
+
YAML.dump(@config, out)
|
221
|
+
end
|
222
|
+
end
|
168
223
|
end
|
169
224
|
|
170
225
|
cli = CLIClient.new
|
171
|
-
cli.parse_arguments(Array(ARGV))
|
226
|
+
cli.parse_arguments(Array(ARGV))
|
data/dacpclient.gemspec
CHANGED
@@ -20,10 +20,15 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.extra_rdoc_files = ['README.md', 'LICENSE']
|
21
21
|
|
22
22
|
spec.add_runtime_dependency 'dnssd', '~> 2.0'
|
23
|
+
spec.add_runtime_dependency 'faraday', '~> 0.8.8'
|
24
|
+
spec.add_runtime_dependency 'plist', '~> 3.1.0'
|
23
25
|
|
24
26
|
spec.add_development_dependency 'yard'
|
25
27
|
spec.add_development_dependency 'redcarpet'
|
26
28
|
spec.add_development_dependency 'github-markup'
|
29
|
+
spec.add_development_dependency 'minitest', '~> 5.2.0'
|
30
|
+
spec.add_development_dependency 'rubocop', '~> 0.15.0'
|
31
|
+
spec.add_development_dependency 'rake'
|
27
32
|
|
28
|
-
spec.required_ruby_version = '>=
|
33
|
+
spec.required_ruby_version = '>= 2.0.0'
|
29
34
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module DACPClient
|
2
|
+
# The Client class handles communication with the server
|
3
|
+
class Bonjour
|
4
|
+
SERVICE_NAME = '_daap._tcp'.freeze
|
5
|
+
DOMAIN = 'local'.freeze
|
6
|
+
|
7
|
+
def browse
|
8
|
+
servers = []
|
9
|
+
|
10
|
+
begin
|
11
|
+
timeout(3) do
|
12
|
+
DNSSD.browse!(SERVICE_NAME, DOMAIN) do |node|
|
13
|
+
ip, port = nil
|
14
|
+
|
15
|
+
resolver = DNSSD::Service.new
|
16
|
+
resolver.resolve(node) do |resolved|
|
17
|
+
ip = get_ip(resolved.target)
|
18
|
+
port = resolved.port
|
19
|
+
|
20
|
+
break unless resolved.flags.more_coming?
|
21
|
+
end
|
22
|
+
|
23
|
+
servers << { name: node.name, ip: ip, port: port, node: node }
|
24
|
+
|
25
|
+
break unless node.flags.more_coming?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
rescue Timeout::Error
|
30
|
+
return []
|
31
|
+
end
|
32
|
+
|
33
|
+
servers
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def get_ip(target)
|
39
|
+
info = Socket.getaddrinfo(target, nil, Socket::AF_INET)
|
40
|
+
info[0][2]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/dacpclient/client.rb
CHANGED
@@ -1,51 +1,89 @@
|
|
1
|
-
require '
|
2
|
-
require 'bundler'
|
3
|
-
Bundler.setup(:default)
|
1
|
+
require 'faraday'
|
4
2
|
require 'digest'
|
5
3
|
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'cgi'
|
6
|
+
require 'plist'
|
6
7
|
require 'dacpclient/pairingserver'
|
7
8
|
require 'dacpclient/dmapparser'
|
8
9
|
require 'dacpclient/dmapbuilder'
|
9
|
-
require '
|
10
|
-
require 'cgi'
|
10
|
+
require 'dacpclient/bonjour'
|
11
11
|
|
12
12
|
module DACPClient
|
13
13
|
# The Client class handles communication with the server
|
14
14
|
class Client
|
15
|
+
attr_accessor :guid, :hsgid
|
16
|
+
attr_reader :name, :host, :port, :session_id
|
17
|
+
|
18
|
+
HOME_SHARING_HOST = 'https://homesharing.itunes.apple.com'
|
19
|
+
HOME_SHARING_PATH = '/WebObjects/MZHomeSharing.woa/wa/getShareIdentifiers'
|
20
|
+
|
21
|
+
DEFAULT_HEADERS = {
|
22
|
+
'Viewer-Only-Client' => '1',
|
23
|
+
# 'Accept-Encoding' => 'gzip',
|
24
|
+
'Connection' => 'keep-alive',
|
25
|
+
'User-Agent' => 'Remote/2.0'
|
26
|
+
}.freeze
|
15
27
|
|
16
28
|
def initialize(name, host = 'localhost', port = 3689)
|
17
29
|
@client = Net::HTTP.new(host, port)
|
18
30
|
@name = name
|
19
31
|
@host = host
|
20
32
|
@port = port
|
21
|
-
|
33
|
+
|
22
34
|
@session_id = nil
|
35
|
+
@hsgid = nil
|
23
36
|
@mediarevision = 1
|
37
|
+
@uri = URI::HTTP.build(host: @host, port: @port)
|
38
|
+
@client = Faraday.new(url: @uri.to_s)
|
39
|
+
end
|
40
|
+
|
41
|
+
def setup_home_sharing(user, password)
|
42
|
+
hs_client = Faraday.new(url: HOME_SHARING_HOST)
|
43
|
+
result = hs_client.post do |request|
|
44
|
+
request.url HOME_SHARING_PATH
|
45
|
+
request.headers['Content-Type'] = 'text/xml'
|
46
|
+
request.headers.merge!(DEFAULT_HEADERS)
|
47
|
+
request.body = { 'appleId' => user, 'guid' => 'empty',
|
48
|
+
'password' => password }.to_plist
|
49
|
+
end
|
50
|
+
response = Plist.parse_xml(result.body)
|
51
|
+
@hsgid = response['sgid']
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_guid
|
55
|
+
return @guid unless @guid.nil?
|
56
|
+
d = Digest::SHA2.hexdigest(@name)
|
57
|
+
d[0..15]
|
24
58
|
end
|
25
59
|
|
26
60
|
def pair(pin)
|
27
|
-
pairingserver = PairingServer.new(
|
61
|
+
pairingserver = PairingServer.new(self, '0.0.0.0', 1024)
|
28
62
|
pairingserver.pin = pin
|
29
63
|
pairingserver.start
|
30
64
|
end
|
31
65
|
|
32
|
-
def self.get_guid(name)
|
33
|
-
d = Digest::SHA2.hexdigest(name)
|
34
|
-
d[0..15]
|
35
|
-
end
|
36
|
-
|
37
66
|
def serverinfo
|
38
|
-
do_action('server-info')
|
67
|
+
do_action('server-info', {}, true)
|
39
68
|
end
|
40
69
|
|
41
|
-
def login
|
42
|
-
|
43
|
-
|
70
|
+
def login
|
71
|
+
response = nil
|
72
|
+
if @hsgid.nil?
|
73
|
+
pairing_guid = '0x' + get_guid
|
74
|
+
response = do_action(:login, 'pairing-guid' => pairing_guid)
|
75
|
+
else
|
76
|
+
response = do_action(:login, 'hasFP' => '1')
|
77
|
+
end
|
44
78
|
@session_id = response[:mlid]
|
45
79
|
response
|
80
|
+
end
|
81
|
+
|
82
|
+
def pair_and_login(pin = nil)
|
83
|
+
login
|
46
84
|
rescue DACPForbiddenError => e
|
47
85
|
pin = 4.times.map { Random.rand(10) } if pin.nil?
|
48
|
-
warn "#{e.result.
|
86
|
+
warn "#{e.result.status} error: Cannot login, starting pairing process"
|
49
87
|
warn "Pincode: #{pin}"
|
50
88
|
pair(pin)
|
51
89
|
retry
|
@@ -71,16 +109,29 @@ module DACPClient
|
|
71
109
|
do_action(:pause)
|
72
110
|
end
|
73
111
|
|
112
|
+
def track_length
|
113
|
+
response = do_action(:getproperty, properties: 'dacp.playingtime')
|
114
|
+
response['cast']
|
115
|
+
end
|
116
|
+
|
74
117
|
def seek(ms)
|
75
118
|
do_action(:setproperty, 'dacp.playingtime' => ms)
|
76
119
|
end
|
77
120
|
|
121
|
+
def get_position
|
122
|
+
response = do_action(:getproperty, properties: 'dacp.playingtime')
|
123
|
+
response['cast'] - response['cant']
|
124
|
+
end
|
125
|
+
|
126
|
+
alias_method :position, :get_position
|
127
|
+
alias_method :position=, :seek
|
128
|
+
|
78
129
|
def status(wait = false)
|
79
130
|
revision = wait ? @mediarevision : 1
|
80
131
|
result = do_action(:playstatusupdate, 'revision-number' => revision)
|
81
132
|
@mediarevision = result[:cmsr]
|
82
133
|
result
|
83
|
-
rescue
|
134
|
+
rescue Faraday::Error::TimeoutError => e
|
84
135
|
if wait
|
85
136
|
retry
|
86
137
|
else
|
@@ -96,6 +147,8 @@ module DACPClient
|
|
96
147
|
do_action(:previtem)
|
97
148
|
end
|
98
149
|
|
150
|
+
alias_method :previous, :prev
|
151
|
+
|
99
152
|
def get_volume
|
100
153
|
response = do_action(:getproperty, properties: 'dmcp.volume')
|
101
154
|
response[:cmvo]
|
@@ -105,6 +158,9 @@ module DACPClient
|
|
105
158
|
do_action(:setproperty, 'dmcp.volume' => volume)
|
106
159
|
end
|
107
160
|
|
161
|
+
alias_method :volume, :get_volume
|
162
|
+
alias_method :volume=, :set_volume
|
163
|
+
|
108
164
|
def get_repeat
|
109
165
|
response = do_action(:getproperty, properties: 'dacp.repeatstate')
|
110
166
|
response[:carp]
|
@@ -123,25 +179,31 @@ module DACPClient
|
|
123
179
|
do_action(:setproperty, 'dmcp.volume' => volume)
|
124
180
|
end
|
125
181
|
|
182
|
+
def getspeakers
|
183
|
+
do_action(:getspeakers)
|
184
|
+
end
|
185
|
+
|
126
186
|
def ctrl_int
|
127
|
-
do_action('ctrl-int', {},
|
187
|
+
do_action('ctrl-int', {}, true)
|
128
188
|
end
|
129
189
|
|
130
190
|
def logout
|
131
|
-
do_action(:logout
|
191
|
+
do_action(:logout)
|
192
|
+
@mediarevision = 1
|
193
|
+
@session_id = nil
|
132
194
|
end
|
133
195
|
|
134
196
|
def queue(id)
|
135
|
-
do_action('playqueue-edit',
|
136
|
-
|
197
|
+
do_action('playqueue-edit', command: 'add',
|
198
|
+
query: "\'dmap.itemid:#{id}\'")
|
137
199
|
end
|
138
200
|
|
139
201
|
def clear_queue
|
140
|
-
do_action('playqueue-edit',
|
202
|
+
do_action('playqueue-edit', command: 'clear')
|
141
203
|
end
|
142
204
|
|
143
205
|
def list_queue
|
144
|
-
do_action('playqueue-contents'
|
206
|
+
do_action('playqueue-contents')
|
145
207
|
end
|
146
208
|
|
147
209
|
def databases
|
@@ -153,7 +215,7 @@ module DACPClient
|
|
153
215
|
end
|
154
216
|
|
155
217
|
def default_db
|
156
|
-
databases[:mlcl].to_a.find {|item| item.mdbk == 1}
|
218
|
+
databases[:mlcl].to_a.find { |item| item.mdbk == 1 }
|
157
219
|
end
|
158
220
|
|
159
221
|
def default_playlist(db)
|
@@ -166,7 +228,7 @@ module DACPClient
|
|
166
228
|
end
|
167
229
|
|
168
230
|
def now_playing_artwork(width = 320, height = 320)
|
169
|
-
do_action(:nowplayingartwork,
|
231
|
+
do_action(:nowplayingartwork, mw: width, mh: height)
|
170
232
|
end
|
171
233
|
|
172
234
|
def search(db, container, search, type = nil)
|
@@ -180,17 +242,19 @@ module DACPClient
|
|
180
242
|
}
|
181
243
|
queries = []
|
182
244
|
type = types.keys if type.nil?
|
183
|
-
Array(type).each do
|
245
|
+
Array(type).each do |t|
|
184
246
|
queries << "'#{types[t]}:#{search}'"
|
185
247
|
end
|
186
|
-
|
187
|
-
#queries.push(words.map { |v| "\'dmap.itemname:*#{v}*\'" }.join('+'))
|
188
|
-
# queries.push(words.map{|v| "\'daap.songartist:*#{v}*\'"}.join('+'))
|
248
|
+
|
189
249
|
q = queries.join(',')
|
190
|
-
meta
|
250
|
+
meta = %w(dmap.itemname dmap.itemid daap.songartist daap.songalbumartist
|
251
|
+
daap.songalbum com.apple.itunes.cloud-id dmap.containeritemid
|
252
|
+
com.apple.itunes.has-video com.apple.itunes.itms-songid
|
253
|
+
com.apple.itunes.extended-media-kind dmap.downloadstatus
|
254
|
+
daap.songdisabled).join(',')
|
191
255
|
|
192
256
|
url = "databases/#{db}/containers/#{container}/items"
|
193
|
-
do_action(url, { type: 'music', sort: 'album', query: q, meta: meta},
|
257
|
+
do_action(url, { type: 'music', sort: 'album', query: q, meta: meta },
|
194
258
|
true)
|
195
259
|
end
|
196
260
|
|
@@ -202,28 +266,23 @@ module DACPClient
|
|
202
266
|
params['session-id'] = @session_id
|
203
267
|
action = '/ctrl-int/1' + action unless cleanurl
|
204
268
|
end
|
205
|
-
params =
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
res = Net::HTTP.new(uri.host, uri.port).start do |http|
|
211
|
-
http.read_timeout = 1000
|
212
|
-
http.request(req)
|
213
|
-
end
|
214
|
-
if res.kind_of?(Net::HTTPServiceUnavailable) ||
|
215
|
-
res.kind_of?(Net::HTTPForbidden)
|
216
|
-
raise DACPForbiddenError.new(res)
|
217
|
-
elsif !res.kind_of?(Net::HTTPSuccess)
|
218
|
-
warn 'No succes!'
|
219
|
-
warn res
|
220
|
-
return nil
|
269
|
+
params['hsgid'] = @hsgid unless @hsgid.nil?
|
270
|
+
result = @client.get do |request|
|
271
|
+
request.url action
|
272
|
+
request.params = params
|
273
|
+
request.headers.merge!(DEFAULT_HEADERS)
|
221
274
|
end
|
222
275
|
|
223
|
-
|
224
|
-
|
276
|
+
parse_result result
|
277
|
+
end
|
278
|
+
|
279
|
+
def parse_result(result)
|
280
|
+
if !result.success?
|
281
|
+
fail DACPForbiddenError, result
|
282
|
+
elsif result.headers['Content-Type'] == 'application/x-dmap-tagged'
|
283
|
+
DMAPParser.parse(result.body)
|
225
284
|
else
|
226
|
-
|
285
|
+
result.body
|
227
286
|
end
|
228
287
|
end
|
229
288
|
end
|
@@ -232,8 +291,8 @@ module DACPClient
|
|
232
291
|
# service unavailable
|
233
292
|
class DACPForbiddenError < StandardError
|
234
293
|
attr_reader :result
|
235
|
-
def initialize(
|
236
|
-
@result =
|
294
|
+
def initialize(result)
|
295
|
+
@result = result
|
237
296
|
end
|
238
297
|
end
|
239
298
|
end
|
@@ -11,31 +11,33 @@ module DACPClient
|
|
11
11
|
new.send(method, *args, &block)
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
def build_container(tag , &block)
|
15
|
+
unless tag.type == :container
|
16
|
+
fail "Tag #{method} is not a container type"
|
17
|
+
end
|
18
|
+
@dmap_stack << TagContainer.new(tag)
|
19
|
+
instance_eval(&block)
|
20
|
+
if @dmap_stack.length > 1
|
21
|
+
@dmap_stack.last.value << @dmap_stack.pop
|
22
|
+
else
|
23
|
+
return @result = @dmap_stack.pop
|
18
24
|
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def method_missing(method, *args, &block)
|
28
|
+
tag = TagDefinition[method]
|
29
|
+
return super if tag.nil?
|
30
|
+
|
19
31
|
if block_given?
|
20
|
-
|
21
|
-
@dmap_stack << DMAPParser::TagContainer.new(tag)
|
22
|
-
instance_eval(&block)
|
23
|
-
if @dmap_stack.length > 1
|
24
|
-
@dmap_stack.last.value << @dmap_stack.pop
|
25
|
-
else
|
26
|
-
return @result = @dmap_stack.pop
|
27
|
-
end
|
28
|
-
else
|
29
|
-
raise "Tag #{method} is not a container type"
|
30
|
-
end
|
32
|
+
build_container(tag, &block)
|
31
33
|
else
|
32
34
|
if @dmap_stack.length > 0
|
33
35
|
args = args.size > 1 ? args : args.first
|
34
|
-
@dmap_stack.last.value <<
|
36
|
+
@dmap_stack.last.value << Tag.new(tag, args)
|
35
37
|
else
|
36
|
-
|
38
|
+
fail 'Cannot build DMAP without a valid container'
|
37
39
|
end
|
38
40
|
end
|
39
41
|
end
|
40
42
|
end
|
41
|
-
end
|
43
|
+
end
|
@@ -2,6 +2,10 @@ module DACPClient
|
|
2
2
|
# The DMAPConverter class converts between binary and ruby formats
|
3
3
|
class DMAPConverter
|
4
4
|
class << self
|
5
|
+
def date_to_bin(data)
|
6
|
+
int_to_bin(value.to_i)
|
7
|
+
end
|
8
|
+
|
5
9
|
def bin_to_byte(data)
|
6
10
|
data.unpack('C').first
|
7
11
|
end
|
@@ -27,15 +31,15 @@ module DACPClient
|
|
27
31
|
end
|
28
32
|
|
29
33
|
def bin_to_hex(data)
|
30
|
-
data.bytes.reduce('') { |a, e| a
|
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))
|
31
39
|
end
|
32
40
|
|
33
41
|
def bool_to_bin(data)
|
34
|
-
|
35
|
-
"\x01"
|
36
|
-
else
|
37
|
-
"\x00"
|
38
|
-
end
|
42
|
+
(data ? 1 : 0).chr
|
39
43
|
end
|
40
44
|
|
41
45
|
def int_to_bin(data)
|
@@ -61,6 +65,55 @@ module DACPClient
|
|
61
65
|
def hex_to_bin(data)
|
62
66
|
[data].pack 'H*'
|
63
67
|
end
|
68
|
+
|
69
|
+
def decode_unknown(data)
|
70
|
+
if data =~ /[^\x20-\x7e]/ # non-readable characters
|
71
|
+
if data.bytesize == 1
|
72
|
+
return DMAPConverter.bin_to_byte(data)
|
73
|
+
elsif data.bytesize == 2
|
74
|
+
return DMAPConverter.bin_to_short(data)
|
75
|
+
elsif data.bytesize == 4
|
76
|
+
return DMAPConverter.bin_to_int(data)
|
77
|
+
elsif data.bytesize == 8
|
78
|
+
return DMAPConverter.bin_to_long(data)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
data
|
82
|
+
end
|
83
|
+
|
84
|
+
def bin_to_string(data)
|
85
|
+
data
|
86
|
+
end
|
87
|
+
alias_method :string_to_bin, :bin_to_string
|
88
|
+
|
89
|
+
alias_method :uint16_to_bin, :short_to_bin
|
90
|
+
alias_method :uint32_to_bin, :int_to_bin
|
91
|
+
alias_method :uint64_to_bin, :long_to_bin
|
92
|
+
|
93
|
+
alias_method :bin_to_uint16, :bin_to_short
|
94
|
+
alias_method :bin_to_uint32, :bin_to_int
|
95
|
+
alias_method :bin_to_uint64, :bin_to_long
|
96
|
+
alias_method :bin_to_unknown, :decode_unknown
|
97
|
+
|
98
|
+
def decode(type, data)
|
99
|
+
decode_method = ('bin_to_' + type.to_s).to_sym
|
100
|
+
if respond_to? decode_method
|
101
|
+
send(decode_method, data)
|
102
|
+
else
|
103
|
+
warn "Decoder: Unknown type #{type}"
|
104
|
+
decode_unknown(data)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def encode(type, data)
|
109
|
+
encode_method = (type.to_s + '_to_bin').to_sym
|
110
|
+
if respond_to? encode_method
|
111
|
+
send(encode_method, data)
|
112
|
+
else
|
113
|
+
warn "Encoder: Unknown type #{type}"
|
114
|
+
data
|
115
|
+
end
|
116
|
+
end
|
64
117
|
end
|
65
118
|
end
|
66
|
-
end
|
119
|
+
end
|