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