dacpclient 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|