dacpclient 0.2.6 → 0.2.9
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 +4 -1
- data/Gemfile +1 -1
- data/Rakefile +8 -6
- data/bin/dacpclient +131 -101
- data/dacpclient.gemspec +3 -1
- data/lib/dacpclient/browser.rb +64 -0
- data/lib/dacpclient/client.rb +77 -49
- data/lib/dacpclient/faraday/flatter_params_encoder.rb +77 -0
- data/lib/dacpclient/model.rb +117 -0
- data/lib/dacpclient/models/pair_info.rb +15 -0
- data/lib/dacpclient/models/play_queue.rb +11 -0
- data/lib/dacpclient/models/play_queue_item.rb +17 -0
- data/lib/dacpclient/models/playlist.rb +9 -0
- data/lib/dacpclient/models/playlists.rb +10 -0
- data/lib/dacpclient/models/status.rb +44 -0
- data/lib/dacpclient/pairingserver.rb +21 -13
- data/lib/dacpclient/version.rb +1 -1
- metadata +42 -12
- data/lib/dacpclient/dmapbuilder.rb +0 -43
- data/lib/dacpclient/dmapconverter.rb +0 -119
- data/lib/dacpclient/dmapparser.rb +0 -40
- data/lib/dacpclient/tag.rb +0 -21
- data/lib/dacpclient/tag_container.rb +0 -51
- data/lib/dacpclient/tag_definition.rb +0 -29
- data/lib/dacpclient/tag_definitions.rb +0 -167
data/lib/dacpclient/client.rb
CHANGED
@@ -1,18 +1,25 @@
|
|
1
|
-
require 'faraday'
|
2
1
|
require 'digest'
|
3
|
-
require 'net/http'
|
4
2
|
require 'uri'
|
5
|
-
require 'cgi'
|
6
3
|
require 'plist'
|
4
|
+
require 'dmapparser'
|
5
|
+
require 'faraday'
|
6
|
+
require 'dacpclient/faraday/flatter_params_encoder'
|
7
7
|
require 'dacpclient/pairingserver'
|
8
|
-
require 'dacpclient/
|
9
|
-
require 'dacpclient/dmapbuilder'
|
8
|
+
require 'dacpclient/browser'
|
10
9
|
require 'dacpclient/version'
|
10
|
+
require 'dacpclient/model'
|
11
|
+
require 'dacpclient/models/status'
|
12
|
+
require 'dacpclient/models/pair_info'
|
13
|
+
require 'dacpclient/models/playlist'
|
14
|
+
require 'dacpclient/models/playlists'
|
15
|
+
require 'dacpclient/models/play_queue_item'
|
16
|
+
require 'dacpclient/models/play_queue'
|
11
17
|
|
12
18
|
module DACPClient
|
13
19
|
# The Client class handles communication with the server
|
14
20
|
class Client
|
15
|
-
attr_accessor :
|
21
|
+
attr_accessor :hsgid
|
22
|
+
attr_writer :guid
|
16
23
|
attr_reader :name, :host, :port, :session_id
|
17
24
|
|
18
25
|
HOME_SHARING_HOST = 'https://homesharing.itunes.apple.com'
|
@@ -26,25 +33,23 @@ module DACPClient
|
|
26
33
|
}.freeze
|
27
34
|
|
28
35
|
def initialize(name, host = 'localhost', port = 3689)
|
29
|
-
@client = Net::HTTP.new(host, port)
|
30
36
|
@name = name
|
31
37
|
@host = host
|
32
38
|
@port = port
|
33
39
|
|
34
40
|
@session_id = nil
|
35
41
|
@hsgid = nil
|
36
|
-
@
|
37
|
-
|
38
|
-
@client = Faraday.new(url: @uri.to_s)
|
42
|
+
@media_revision = 1
|
43
|
+
setup_connection
|
39
44
|
end
|
40
45
|
|
41
|
-
[:play, :playpause, :stop, :pause,
|
46
|
+
[:play, :playpause, :stop, :pause,
|
42
47
|
:nextitem, :previtem, :getspeakers].each do |action_name|
|
43
|
-
define_method action_name do
|
48
|
+
define_method action_name do
|
44
49
|
do_action action_name
|
45
50
|
end
|
46
51
|
end
|
47
|
-
|
52
|
+
|
48
53
|
alias_method :previous, :previtem
|
49
54
|
alias_method :prev, :previtem
|
50
55
|
alias_method :next, :nextitem
|
@@ -70,22 +75,22 @@ module DACPClient
|
|
70
75
|
end
|
71
76
|
|
72
77
|
def pair(pin)
|
73
|
-
pairingserver = PairingServer.new(
|
78
|
+
pairingserver = PairingServer.new(name, guid)
|
74
79
|
pairingserver.pin = pin
|
75
80
|
pairingserver.start
|
76
81
|
end
|
77
82
|
|
78
83
|
def serverinfo
|
79
|
-
do_action('server-info',
|
84
|
+
do_action('server-info', clean_url: true)
|
80
85
|
end
|
81
86
|
|
82
87
|
def login
|
83
88
|
response = nil
|
84
89
|
if @hsgid.nil?
|
85
90
|
pairing_guid = '0x' + guid
|
86
|
-
response = do_action(:login,
|
91
|
+
response = do_action(:login, :'pairing-guid' => pairing_guid)
|
87
92
|
else
|
88
|
-
response = do_action(:login,
|
93
|
+
response = do_action(:login, hasFP: 1)
|
89
94
|
end
|
90
95
|
@session_id = response[:mlid]
|
91
96
|
response
|
@@ -93,16 +98,22 @@ module DACPClient
|
|
93
98
|
|
94
99
|
def pair_and_login(pin = nil)
|
95
100
|
login
|
96
|
-
rescue DACPForbiddenError => e
|
101
|
+
rescue DACPForbiddenError, Faraday::ConnectionFailed => e
|
97
102
|
pin = 4.times.map { Random.rand(10) } if pin.nil?
|
98
|
-
|
103
|
+
if e.instance_of? DACPForbiddenError
|
104
|
+
message = e.result.status
|
105
|
+
else
|
106
|
+
message = e
|
107
|
+
end
|
108
|
+
warn "#{message} error: Cannot login, starting pairing process"
|
99
109
|
warn "Pincode: #{pin}"
|
100
|
-
pair(pin)
|
110
|
+
@host = pair(pin).host
|
111
|
+
setup_connection
|
101
112
|
retry
|
102
113
|
end
|
103
114
|
|
104
115
|
def content_codes
|
105
|
-
do_action('content-codes',
|
116
|
+
do_action('content-codes', clean_url: true)
|
106
117
|
end
|
107
118
|
|
108
119
|
def track_length
|
@@ -122,9 +133,10 @@ module DACPClient
|
|
122
133
|
alias_method :position=, :seek
|
123
134
|
|
124
135
|
def status(wait = false)
|
125
|
-
revision = wait ? @
|
126
|
-
result = do_action(:playstatusupdate, 'revision-number' => revision
|
127
|
-
|
136
|
+
revision = wait ? @media_revision : 1
|
137
|
+
result = do_action(:playstatusupdate, :'revision-number' => revision,
|
138
|
+
model: Status)
|
139
|
+
@media_revision = result.media_revision
|
128
140
|
result
|
129
141
|
rescue Faraday::Error::TimeoutError => e
|
130
142
|
if wait
|
@@ -140,7 +152,7 @@ module DACPClient
|
|
140
152
|
end
|
141
153
|
|
142
154
|
def volume=(volume)
|
143
|
-
do_action(:setproperty, 'dmcp.volume' => volume)
|
155
|
+
do_action(:setproperty, :'dmcp.volume' => volume)
|
144
156
|
end
|
145
157
|
|
146
158
|
def repeat
|
@@ -162,12 +174,12 @@ module DACPClient
|
|
162
174
|
end
|
163
175
|
|
164
176
|
def ctrl_int
|
165
|
-
do_action('ctrl-int',
|
177
|
+
do_action('ctrl-int', clean_url: true)
|
166
178
|
end
|
167
179
|
|
168
180
|
def logout
|
169
181
|
do_action(:logout)
|
170
|
-
@
|
182
|
+
@media_revision = 1
|
171
183
|
@session_id = nil
|
172
184
|
end
|
173
185
|
|
@@ -181,35 +193,37 @@ module DACPClient
|
|
181
193
|
end
|
182
194
|
|
183
195
|
def list_queue
|
184
|
-
do_action('playqueue-contents')
|
196
|
+
do_action('playqueue-contents', model: PlayQueue)
|
185
197
|
end
|
186
198
|
|
187
199
|
def databases
|
188
|
-
do_action('databases',
|
200
|
+
do_action('databases', clean_url: true)
|
189
201
|
end
|
190
202
|
|
191
|
-
def playlists(db)
|
192
|
-
do_action("databases/#{db}/containers",
|
203
|
+
def playlists(db = default_db)
|
204
|
+
do_action("databases/#{db.miid}/containers", clean_url: true,
|
205
|
+
model: Playlists).items
|
193
206
|
end
|
194
207
|
|
195
208
|
def default_db
|
196
|
-
databases
|
209
|
+
databases.mlcl.to_a.find { |item| item.mdbk == 1 }
|
197
210
|
end
|
198
211
|
|
199
|
-
def default_playlist(db)
|
200
|
-
|
212
|
+
def default_playlist(db = default_db)
|
213
|
+
playlists(db).find { |item| item.base_playlist? }
|
201
214
|
end
|
202
215
|
|
203
216
|
def artwork(database, id, width = 320, height = 320)
|
204
217
|
url = "databases/#{database}/items/#{id}/extra_data/artwork"
|
205
|
-
do_action(url, { mw: width, mh: height }, true)
|
218
|
+
do_action(url, { mw: width, mh: height }, clean_url: true)
|
206
219
|
end
|
207
220
|
|
208
221
|
def now_playing_artwork(width = 320, height = 320)
|
209
222
|
do_action(:nowplayingartwork, mw: width, mh: height)
|
210
223
|
end
|
211
224
|
|
212
|
-
def search(
|
225
|
+
def search(search, type = nil, db = default_db,
|
226
|
+
container = default_playlist(default_db))
|
213
227
|
search = URI.escape(search)
|
214
228
|
types = {
|
215
229
|
title: 'dmap.itemname',
|
@@ -225,40 +239,54 @@ module DACPClient
|
|
225
239
|
end
|
226
240
|
|
227
241
|
q = queries.join(',')
|
228
|
-
|
242
|
+
q = '(' + q + ')' if queries.length > 1
|
243
|
+
meta = %w(dmap.itemname dmap.itemid com.apple.itunes.has-chapter-data
|
229
244
|
daap.songalbum com.apple.itunes.cloud-id dmap.containeritemid
|
230
245
|
com.apple.itunes.has-video com.apple.itunes.itms-songid
|
231
246
|
com.apple.itunes.extended-media-kind dmap.downloadstatus
|
232
|
-
daap.songdisabled
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
247
|
+
daap.songdisabled daap.songhasbeenplayed daap.songbookmark
|
248
|
+
com.apple.itunes.is-hd-video daap.songlongcontentdescription
|
249
|
+
daap.songtime daap.songuserplaycount daap.songartist
|
250
|
+
com.apple.itunes.content-rating daap.songdatereleased
|
251
|
+
com.apple.itunes.movie-info-xml daap.songalbumartist
|
252
|
+
com.apple.itunes.extended-media-kind).join(',')
|
253
|
+
url = "databases/#{db.miid}/containers/#{container.miid}/items"
|
254
|
+
do_action(url, { query: q, type: 'music', sort: 'album', meta: meta,
|
255
|
+
:'include-sort-headers' => 1 }, clean_url: true)
|
237
256
|
end
|
238
257
|
|
239
258
|
private
|
240
259
|
|
241
|
-
def
|
260
|
+
def setup_connection
|
261
|
+
@uri = URI::HTTP.build(host: @host, port: @port)
|
262
|
+
Faraday::Utils.default_params_encoder = Faraday::FlatterParamsEncoder
|
263
|
+
@client = Faraday.new(@uri.to_s)
|
264
|
+
end
|
265
|
+
|
266
|
+
def do_action(action, clean_url: false, model: nil, **params)
|
242
267
|
action = '/' + action.to_s
|
243
268
|
unless @session_id.nil?
|
244
|
-
params['session-id'] = @session_id
|
245
|
-
action = '/ctrl-int/1' + action unless
|
269
|
+
params['session-id'] = @session_id.to_s
|
270
|
+
action = '/ctrl-int/1' + action unless clean_url
|
246
271
|
end
|
247
272
|
params['hsgid'] = @hsgid unless @hsgid.nil?
|
273
|
+
|
248
274
|
result = @client.get do |request|
|
275
|
+
request.options.params_encoder = Faraday::FlatterParamsEncoder
|
249
276
|
request.url action
|
250
277
|
request.params = params
|
251
278
|
request.headers.merge!(DEFAULT_HEADERS)
|
252
279
|
end
|
253
280
|
|
254
|
-
parse_result result
|
281
|
+
parse_result result, model
|
255
282
|
end
|
256
|
-
|
257
|
-
def parse_result(result)
|
283
|
+
|
284
|
+
def parse_result(result, model)
|
258
285
|
if !result.success?
|
259
286
|
fail DACPForbiddenError, result
|
260
287
|
elsif result.headers['Content-Type'] == 'application/x-dmap-tagged'
|
261
|
-
DMAPParser.parse(result.body)
|
288
|
+
res = DMAPParser::Parser.parse(result.body)
|
289
|
+
model ? model.new(res) : res
|
262
290
|
else
|
263
291
|
result.body
|
264
292
|
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# rubocop:disable all
|
2
|
+
require 'cgi'
|
3
|
+
module Faraday
|
4
|
+
module FlatterParamsEncoder
|
5
|
+
def self.escape(s)
|
6
|
+
s.to_s.gsub(/[^a-zA-Z0-9 .~_\-,:\*'\+()]/) do
|
7
|
+
'%' + $&.unpack('H2' * $&.bytesize).join('%').upcase
|
8
|
+
end.tr(' ', '+')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.unescape(s)
|
12
|
+
CGI.unescape(s.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.encode(params)
|
16
|
+
return nil if params.nil?
|
17
|
+
|
18
|
+
unless params.is_a?(Array)
|
19
|
+
unless params.respond_to?(:to_hash)
|
20
|
+
fail TypeError,
|
21
|
+
"Can't convert #{params.class} into Hash."
|
22
|
+
end
|
23
|
+
params = params.to_hash
|
24
|
+
params = params.map do |key, value|
|
25
|
+
key = key.to_s if key.kind_of?(Symbol)
|
26
|
+
[key, value]
|
27
|
+
end
|
28
|
+
# Useful default for OAuth and caching.
|
29
|
+
# Only to be used for non-Array inputs. Arrays should preserve order.
|
30
|
+
params.sort!
|
31
|
+
end
|
32
|
+
|
33
|
+
# The params have form [['key1', 'value1'], ['key2', 'value2']].
|
34
|
+
buffer = ''
|
35
|
+
params.each do |key, value|
|
36
|
+
encoded_key = escape(key)
|
37
|
+
value = value.to_s if value == true || value == false
|
38
|
+
if value.nil?
|
39
|
+
buffer << "#{encoded_key}&"
|
40
|
+
elsif value.kind_of?(Array)
|
41
|
+
value.each do |sub_value|
|
42
|
+
encoded_value = escape(sub_value)
|
43
|
+
buffer << "#{encoded_key}=#{encoded_value}&"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
encoded_value = escape(value)
|
47
|
+
buffer << "#{encoded_key}=#{encoded_value}&"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
buffer.chop
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.decode(query)
|
54
|
+
empty_accumulator = {}
|
55
|
+
return nil if query.nil?
|
56
|
+
split_query = (query.split('&').map do |pair|
|
57
|
+
pair.split('=', 2) if pair && !pair.empty?
|
58
|
+
end).compact
|
59
|
+
split_query.reduce(empty_accumulator.dup) do |accu, pair|
|
60
|
+
pair[0] = unescape(pair[0])
|
61
|
+
pair[1] = true if pair[1].nil?
|
62
|
+
if pair[1].respond_to?(:to_str)
|
63
|
+
pair[1] = unescape(pair[1].to_str.gsub(/\+/, ' '))
|
64
|
+
end
|
65
|
+
if accu[pair[0]].kind_of?(Array)
|
66
|
+
accu[pair[0]] << pair[1]
|
67
|
+
elsif accu[pair[0]]
|
68
|
+
accu[pair[0]] = [accu[pair[0]], pair[1]]
|
69
|
+
else
|
70
|
+
accu[pair[0]] = pair[1]
|
71
|
+
end
|
72
|
+
accu
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# rubocop:enable all
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class Model
|
3
|
+
class DMAPAttribute < Struct.new(:tag, :item_class, :value)
|
4
|
+
def initialize(tag, item_class = nil)
|
5
|
+
super tag, item_class, nil
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(params = {})
|
10
|
+
if params.is_a? DMAPParser::TagContainer
|
11
|
+
deserialize(params)
|
12
|
+
elsif params
|
13
|
+
params.each do |attr, value|
|
14
|
+
public_send("#{attr}=", value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
puts self.class.name
|
21
|
+
dmap_attributes.each do |key, value|
|
22
|
+
puts " #{key}: #{value.value}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
"#<#{self.class.name} " + dmap_attributes.map do |key, value|
|
28
|
+
"#{key}: #{value.value}"
|
29
|
+
end.join(', ') + '>'
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_dmap
|
33
|
+
attributes = dmap_attributes
|
34
|
+
DMAPParser::Builder.send dmap_tag do
|
35
|
+
attributes.values.each do |value|
|
36
|
+
send(value.tag, value.value)
|
37
|
+
end
|
38
|
+
end.to_dmap
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method, *args, &block)
|
42
|
+
if method.to_s =~ /(.*)\=$/ &&
|
43
|
+
dmap_attributes.key?(Regexp.last_match[1].to_sym)
|
44
|
+
dmap_attributes[Regexp.last_match[1].to_sym].value = args.first
|
45
|
+
elsif method.to_s =~ /(.*)\?$/ &&
|
46
|
+
dmap_attributes.key?(Regexp.last_match[1].to_sym)
|
47
|
+
dmap_attributes[Regexp.last_match[1].to_sym].value
|
48
|
+
elsif dmap_attributes.key? method
|
49
|
+
dmap_attributes[method].value
|
50
|
+
else
|
51
|
+
super
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class << self
|
56
|
+
def dmap_attribute(method, key)
|
57
|
+
@dmap_attributes ||= {}
|
58
|
+
@dmap_attributes[method] = key
|
59
|
+
end
|
60
|
+
|
61
|
+
def dmap_container(method, key, item_class)
|
62
|
+
@dmap_attributes ||= {}
|
63
|
+
@dmap_attributes[method] = [key, item_class]
|
64
|
+
end
|
65
|
+
|
66
|
+
def dmap_tag(tag = nil)
|
67
|
+
if tag
|
68
|
+
@dmap_tag = tag
|
69
|
+
else
|
70
|
+
@dmap_tag
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_dmap(params = {})
|
75
|
+
new(params).to_dmap
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def deserialize(data)
|
82
|
+
warn 'Invalid tag' if data.type.tag.to_sym != dmap_tag
|
83
|
+
dmap_attributes.values.each do |value|
|
84
|
+
value.value = get_value(data, value) if data.respond_to? value.tag
|
85
|
+
end
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def get_value(data, value)
|
90
|
+
item_class = value.item_class
|
91
|
+
if item_class
|
92
|
+
data.send(value.tag).to_a.map do |item|
|
93
|
+
item_class.new(item) if item_class.dmap_tag == item.type.tag.to_sym
|
94
|
+
end.compact
|
95
|
+
else
|
96
|
+
data.send(value.tag)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def dmap_attributes
|
101
|
+
@dmap_attributes ||= initialize_attributes
|
102
|
+
end
|
103
|
+
|
104
|
+
def initialize_attributes
|
105
|
+
class_attributes = self.class.instance_variable_get(:@dmap_attributes)
|
106
|
+
attributes = {}
|
107
|
+
class_attributes.map do |key, value|
|
108
|
+
attributes[key] = DMAPAttribute.new(*value)
|
109
|
+
end
|
110
|
+
attributes
|
111
|
+
end
|
112
|
+
|
113
|
+
def dmap_tag
|
114
|
+
self.class.instance_variable_get(:@dmap_tag)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class PairInfo < Model
|
3
|
+
dmap_tag :cmpa
|
4
|
+
|
5
|
+
dmap_attribute :pairing_code, :cmpg
|
6
|
+
dmap_attribute :name, :cmnm
|
7
|
+
dmap_attribute :type, :cmty
|
8
|
+
|
9
|
+
# DMAPParser::Builder.cmpa do
|
10
|
+
# cmpg pair
|
11
|
+
# cmnm name
|
12
|
+
# cmty device_type
|
13
|
+
# end.to_dmap
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class PlayQueue < Model
|
3
|
+
dmap_tag :ceQR
|
4
|
+
dmap_attribute :status, :mstt
|
5
|
+
dmap_attribute :container_count, :mtco
|
6
|
+
dmap_attribute :shuffle_mode, :apsm
|
7
|
+
dmap_attribute :repeat_mode, :aprm
|
8
|
+
# ceQu (unknown (1): unknown): 0
|
9
|
+
dmap_container :items, :mlcl, DACPClient::PlayQueueItem
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class PlayQueueItem < Model
|
3
|
+
dmap_tag :mlit
|
4
|
+
dmap_attribute :track_id, :ceQs
|
5
|
+
dmap_attribute :title, :ceQn
|
6
|
+
dmap_attribute :artist, :ceQr
|
7
|
+
dmap_attribute :album, :ceQa
|
8
|
+
dmap_attribute :genre, :ceQg
|
9
|
+
dmap_attribute :album_id, :asai
|
10
|
+
|
11
|
+
dmap_attribute :media_kind, :cmmk
|
12
|
+
dmap_attribute :song_time, :astm
|
13
|
+
|
14
|
+
# aeGs (com.apple.itunes.can-be-genius-seed: bool): true
|
15
|
+
# ceGS (com.apple.itunes.genius-selectable: bool): true
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class Playlists < Model
|
3
|
+
dmap_tag :aply
|
4
|
+
dmap_attribute :status, :mstt
|
5
|
+
dmap_attribute :update_type, :myty
|
6
|
+
dmap_attribute :container_count, :mtco
|
7
|
+
dmap_attribute :returned_count, :mrco
|
8
|
+
dmap_container :items, :mlcl, DACPClient::Playlist
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DACPClient
|
2
|
+
class Status < Model
|
3
|
+
dmap_tag :cmst
|
4
|
+
dmap_attribute :media_revision, :cmsr
|
5
|
+
dmap_attribute :status_code, :mstt
|
6
|
+
dmap_attribute :play_status, :caps
|
7
|
+
dmap_attribute :shuffle_state, :cash
|
8
|
+
dmap_attribute :repeat_state, :carp
|
9
|
+
dmap_attribute :fullscreen, :cafs
|
10
|
+
dmap_attribute :visualizer, :cavs
|
11
|
+
dmap_attribute :volume_controllable, :cavc
|
12
|
+
dmap_attribute :album_shuffle, :caas
|
13
|
+
dmap_attribute :album_repeat, :caar
|
14
|
+
dmap_attribute :fullscreen_enabled, :cafe
|
15
|
+
dmap_attribute :visualizer_enabled, :cave
|
16
|
+
dmap_attribute :track_id, :canp
|
17
|
+
dmap_attribute :title, :cann
|
18
|
+
dmap_attribute :artist, :cana
|
19
|
+
dmap_attribute :album, :canl
|
20
|
+
dmap_attribute :album_id, :asai
|
21
|
+
dmap_attribute :media_kind, :cmmk
|
22
|
+
dmap_attribute :song_time, :astm
|
23
|
+
dmap_attribute :song_length, :cast
|
24
|
+
dmap_attribute :song_remaining_time, :cant
|
25
|
+
|
26
|
+
def song_position
|
27
|
+
return 0 unless song_length? && song_remaining_time?
|
28
|
+
song_length - song_remaining_time
|
29
|
+
end
|
30
|
+
|
31
|
+
def stopped?
|
32
|
+
play_status == 2
|
33
|
+
end
|
34
|
+
|
35
|
+
def playing?
|
36
|
+
play_status == 4
|
37
|
+
end
|
38
|
+
|
39
|
+
def paused?
|
40
|
+
!stopped? && !playing?
|
41
|
+
end
|
42
|
+
# casu (dacp.su: byte): 0
|
43
|
+
end
|
44
|
+
end
|
@@ -2,19 +2,22 @@ require 'socket'
|
|
2
2
|
require 'dnssd'
|
3
3
|
require 'digest'
|
4
4
|
require 'gserver'
|
5
|
+
require 'dmapparser/builder'
|
5
6
|
module DACPClient
|
6
7
|
# The pairingserver handles pairing with iTunes
|
7
8
|
class PairingServer < GServer
|
8
9
|
attr_accessor :pin, :device_type
|
10
|
+
attr_reader :peer
|
9
11
|
|
10
12
|
MDNS_TYPE = '_touch-remote._tcp'.freeze
|
11
13
|
|
12
|
-
def initialize(
|
13
|
-
@name =
|
14
|
+
def initialize(name, guid, host = '0.0.0.0', port = 1024)
|
15
|
+
@name = name
|
14
16
|
@port = port
|
15
17
|
@host = host
|
16
|
-
@pair =
|
18
|
+
@pair = guid
|
17
19
|
@pin = [0, 0, 0, 0]
|
20
|
+
@peer = nil
|
18
21
|
@device_type = 'iPod'
|
19
22
|
super port, host
|
20
23
|
end
|
@@ -23,24 +26,33 @@ module DACPClient
|
|
23
26
|
@pairing_string = generate_pairing_string(@pair, @name, @device_type)
|
24
27
|
@expected = PairingServer.generate_pin_challenge(@pair, @pin)
|
25
28
|
@service = DNSSD.register!(@name, MDNS_TYPE, 'local', @port, text_record)
|
26
|
-
|
29
|
+
@pairing_string
|
30
|
+
PairInfo.new(DMAPParser::Parser.parse(@pairing_string))
|
27
31
|
super
|
28
32
|
join
|
29
33
|
|
30
34
|
@service.stop
|
31
35
|
|
32
36
|
sleep 0.5 # sleep so iTunes accepts our login
|
37
|
+
peer
|
33
38
|
end
|
34
39
|
|
35
40
|
def self.generate_pin_challenge(pair, pin)
|
36
41
|
pin_string = pin.map { |i| "#{i}\x00" }.join
|
37
|
-
Digest::MD5.hexdigest(pair.upcase + pin_string)
|
42
|
+
Digest::MD5.hexdigest(pair.upcase + pin_string).upcase
|
38
43
|
end
|
39
44
|
|
40
45
|
def serve(client)
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
data = client.gets
|
47
|
+
peer_addr = client.peeraddr[2]
|
48
|
+
browser = DACPClient::Browser.new
|
49
|
+
browser.browse
|
50
|
+
@peer = browser.devices.find do |device|
|
51
|
+
device.host == peer_addr
|
52
|
+
end
|
53
|
+
if data =~ /pairingcode=#{@expected}/i && @peer
|
54
|
+
client.print "HTTP/1.1 200 OK\n" +
|
55
|
+
"Content-Length: #{@pairing_string.length}\n\n"
|
44
56
|
client.print @pairing_string
|
45
57
|
client.close
|
46
58
|
stop
|
@@ -64,11 +76,7 @@ module DACPClient
|
|
64
76
|
end
|
65
77
|
|
66
78
|
def generate_pairing_string(pair, name, device_type)
|
67
|
-
|
68
|
-
cmpg pair
|
69
|
-
cmnm name
|
70
|
-
cmty device_type
|
71
|
-
end.to_dmap
|
79
|
+
PairInfo.build_dmap(pairing_code: pair, name: name, type: device_type)
|
72
80
|
end
|
73
81
|
end
|
74
82
|
end
|
data/lib/dacpclient/version.rb
CHANGED