dacpclient 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +15 -0
- data/.rubocop.yml +3 -1
- data/Gemfile +4 -5
- data/README.md +47 -2
- data/{remoteclient.rb → bin/dacpclient} +51 -6
- data/dacpclient.gemspec +4 -3
- data/lib/dacpclient/client.rb +72 -29
- data/lib/dacpclient/dmapbuilder.rb +8 -3
- data/lib/dacpclient/dmapconverter.rb +1 -0
- data/lib/dacpclient/dmapparser.rb +70 -66
- data/lib/dacpclient/pairingserver.rb +31 -21
- data/lib/dacpclient/tagdefinitions.rb +30 -18
- data/lib/dacpclient/version.rb +2 -1
- data/lib/dacpclient.rb +0 -1
- metadata +9 -10
- data/Gemfile.lock +0 -59
- data/TODO +0 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a91f6ef4321ce83e0cabc85493552ebf7961756
|
4
|
+
data.tar.gz: c032e8102b42801e7fa220861ecefb5b306f0550
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 352cf3561c859d1bf52deb7a3fb33d036245282e6610e72871f11863055afebd3fb41ff630fdb887c3f52573bf3ead3fbcf9fc7163ca0b6882391d9addf65ec6
|
7
|
+
data.tar.gz: e78e5bad668a556c7d583702c23798a75b2df288bfb8358c9e750047685963bfbbc3380f9c2df6c12fb9236a3b5c2bb65f9b8bd5ebc6950a9c3801754cac72f6
|
data/.gitignore
ADDED
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
ruby '2.0.0'
|
1
|
+
# ruby '2.0.0'
|
2
2
|
gemspec
|
3
3
|
|
4
4
|
source 'https://rubygems.org'
|
@@ -6,9 +6,8 @@ source 'https://rubygems.org'
|
|
6
6
|
group :test do
|
7
7
|
gem 'pry'
|
8
8
|
gem 'minitest', '~> 5.0.6'
|
9
|
-
#gem 'coveralls', require: false
|
9
|
+
# gem 'coveralls', require: false
|
10
10
|
gem 'rake'
|
11
|
-
gem 'rubocop',
|
12
|
-
gem 'simplecov', require
|
13
|
-
#gem 'simple_mock'
|
11
|
+
gem 'rubocop', '~> 0.11.1'
|
12
|
+
gem 'simplecov', :require => false
|
14
13
|
end
|
data/README.md
CHANGED
@@ -1,3 +1,48 @@
|
|
1
|
-
|
1
|
+
#DACPClient
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/dacpclient.png)](http://badge.fury.io/rb/dacpclient) [![Dependency Status](https://gemnasium.com/jurriaan/ruby-dacpclient.png)](https://gemnasium.com/jurriaan/ruby-dacpclient)
|
4
|
+
|
5
|
+
A DACP (iTunes Remote protocol) client written in the wonderful Ruby language.
|
6
|
+
You can use this for controlling iTunes. It uses the same protocol as the iTunes remote iOS app.
|
7
|
+
|
8
|
+
Look at the [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient) file for an example client.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
gem 'dacpclient'
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
bundle
|
19
|
+
|
20
|
+
Or install it yourself using:
|
21
|
+
|
22
|
+
gem install dacpclient
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
See [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient)
|
27
|
+
|
28
|
+
## Todo
|
29
|
+
|
30
|
+
- Add tests
|
31
|
+
- Add more tagdefinitions
|
32
|
+
- Documentation
|
33
|
+
|
34
|
+
## Contributing
|
35
|
+
|
36
|
+
1. Fork it
|
37
|
+
2. Create your feature branch (`git checkout -tb my-new-feature`)
|
38
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
39
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
40
|
+
5. Create new Pull Request
|
41
|
+
|
42
|
+
## Contributors
|
43
|
+
|
44
|
+
- [Jurriaan Pruis](https://github.com/jurriaan)
|
45
|
+
|
46
|
+
## Thanks
|
47
|
+
|
48
|
+
- [edc1591](https://github.com/edc1591) - for some of the 'Up Next' code
|
@@ -1,8 +1,10 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'dacpclient'
|
3
4
|
require 'english'
|
4
5
|
require 'socket'
|
5
|
-
|
6
|
+
|
7
|
+
# This is the CLI DACP Client. Normally installed as `dacpclient`
|
6
8
|
class CLIClient
|
7
9
|
def initialize
|
8
10
|
@client = DACPClient::Client.new("CLIClient (#{Socket.gethostname})", 'localhost', 3689)
|
@@ -13,7 +15,7 @@ class CLIClient
|
|
13
15
|
if arguments.length > 0 && !([:parse_arguments].include?(arguments.first.to_sym)) && self.class.instance_methods(false).include?(arguments.first.to_sym)
|
14
16
|
method = arguments.first.to_sym
|
15
17
|
send method
|
16
|
-
status unless
|
18
|
+
status unless [:help, :usage, :status, :status_ticker].include?(method)
|
17
19
|
else
|
18
20
|
usage
|
19
21
|
end
|
@@ -28,11 +30,37 @@ class CLIClient
|
|
28
30
|
name = status.cann
|
29
31
|
artist = status.cana
|
30
32
|
album = status.canl
|
31
|
-
playstatus = status.caps != 4 ? '
|
33
|
+
playstatus = status.caps != 4 ? '❙❙' : '▶ '
|
32
34
|
remaining = status.cant
|
33
35
|
total = status.cast
|
34
36
|
current = total - remaining
|
35
|
-
puts "[#{
|
37
|
+
puts "[#{format_time(current)}/#{format_time(total)}] #{playstatus} #{name} - #{artist} (#{album})"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def status_ticker
|
42
|
+
login
|
43
|
+
status = nil
|
44
|
+
status_time = 0
|
45
|
+
repeat_every(1) do
|
46
|
+
unless status.nil?
|
47
|
+
if status.caps == 2
|
48
|
+
print "\r\033[K[STOPPED]"
|
49
|
+
else
|
50
|
+
name = status.cann
|
51
|
+
artist = status.cana
|
52
|
+
album = status.canl
|
53
|
+
playstatus = status.caps != 4 ? '❙❙' : '▶ '
|
54
|
+
total = status.cast
|
55
|
+
remaining = status.cant
|
56
|
+
current = total - remaining + [((Time.now.to_f * 1000.0) - status_time), 0].max
|
57
|
+
print "\r\033[K[#{format_time(current)}/#{format_time(total)}] #{playstatus} #{name} - #{artist} (#{album})"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
loop do
|
62
|
+
status = @client.status true
|
63
|
+
status_time = Time.now.to_f * 1000.0
|
36
64
|
end
|
37
65
|
end
|
38
66
|
|
@@ -86,6 +114,11 @@ class CLIClient
|
|
86
114
|
puts
|
87
115
|
end
|
88
116
|
|
117
|
+
def stop
|
118
|
+
login
|
119
|
+
@client.stop
|
120
|
+
end
|
121
|
+
|
89
122
|
def debug
|
90
123
|
login
|
91
124
|
require 'pry'
|
@@ -94,6 +127,7 @@ class CLIClient
|
|
94
127
|
|
95
128
|
def usage
|
96
129
|
puts "Usage: #{$PROGRAM_NAME} [command]"
|
130
|
+
puts "(c) #{Time.now.year} Jurriaan Pruis <email@jurriaanpruis.nl>"
|
97
131
|
puts
|
98
132
|
puts 'Where command is one of the following:'
|
99
133
|
|
@@ -120,6 +154,17 @@ class CLIClient
|
|
120
154
|
sprintf('%02d:%02d:%02d', hours, minutes, seconds)
|
121
155
|
end
|
122
156
|
end
|
157
|
+
|
158
|
+
def repeat_every(interval)
|
159
|
+
Thread.new do
|
160
|
+
loop do
|
161
|
+
start_time = Time.now
|
162
|
+
yield
|
163
|
+
elapsed = Time.now - start_time
|
164
|
+
sleep([interval - elapsed, 0].max)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
123
168
|
end
|
124
169
|
|
125
170
|
cli = CLIClient.new
|
data/dacpclient.gemspec
CHANGED
@@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
|
|
7
7
|
spec.version = DACPClient::VERSION
|
8
8
|
spec.authors = ['Jurriaan Pruis']
|
9
9
|
spec.email = ['email@jurriaanpruis.nl']
|
10
|
-
spec.description = 'A DACP (iTunes Remote protocol) client
|
11
|
-
spec.summary = 'A DACP (iTunes Remote protocol) client
|
10
|
+
spec.description = 'A DACP (iTunes Remote protocol) client'
|
11
|
+
spec.summary = 'A DACP (iTunes Remote protocol) client'
|
12
12
|
spec.homepage = 'https://github.com/jurriaan/ruby-dacpclient'
|
13
13
|
spec.license = 'MIT'
|
14
14
|
spec.platform = Gem::Platform::RUBY
|
@@ -24,5 +24,6 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency 'yard'
|
25
25
|
spec.add_development_dependency 'redcarpet'
|
26
26
|
spec.add_development_dependency 'github-markup'
|
27
|
-
|
27
|
+
|
28
|
+
spec.required_ruby_version = '>= 1.9.3'
|
28
29
|
end
|
data/lib/dacpclient/client.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
-
$LOAD_PATH.unshift __dir__
|
2
1
|
require 'rubygems'
|
3
|
-
require 'bundler
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup(:default)
|
4
4
|
require 'digest'
|
5
5
|
require 'net/http'
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
require 'dacpclient/pairingserver'
|
7
|
+
require 'dacpclient/dmapparser'
|
8
|
+
require 'dacpclient/dmapbuilder'
|
9
9
|
require 'uri'
|
10
10
|
require 'cgi'
|
11
11
|
|
12
12
|
module DACPClient
|
13
|
+
# The Client class handles communication with the server
|
13
14
|
class Client
|
14
15
|
|
15
16
|
def initialize(name, host = 'localhost', port = 3689)
|
@@ -22,9 +23,9 @@ module DACPClient
|
|
22
23
|
@mediarevision = 1
|
23
24
|
end
|
24
25
|
|
25
|
-
def pair(pin
|
26
|
+
def pair(pin)
|
26
27
|
pairingserver = PairingServer.new(@name, '0.0.0.0', 1024)
|
27
|
-
pairingserver.pin = pin
|
28
|
+
pairingserver.pin = pin
|
28
29
|
pairingserver.start
|
29
30
|
end
|
30
31
|
|
@@ -38,12 +39,14 @@ module DACPClient
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def login(pin = nil)
|
41
|
-
|
42
|
+
pairing_guid = '0x' + Client.get_guid(@name)
|
43
|
+
response = do_action(:login, { 'pairing-guid' => pairing_guid })
|
42
44
|
@session_id = response[:mlid]
|
43
45
|
response
|
44
46
|
rescue DACPForbiddenError => e
|
45
|
-
puts "#{e.result.message} error: Cannot login, starting pairing process"
|
46
47
|
pin = 4.times.map { Random.rand(10) } if pin.nil?
|
48
|
+
warn "#{e.result.message} error: Cannot login, starting pairing process"
|
49
|
+
warn "Pincode: #{pin}"
|
47
50
|
pair(pin)
|
48
51
|
retry
|
49
52
|
end
|
@@ -77,6 +80,12 @@ module DACPClient
|
|
77
80
|
result = do_action(:playstatusupdate, 'revision-number' => revision)
|
78
81
|
@mediarevision = result[:cmsr]
|
79
82
|
result
|
83
|
+
rescue Net::ReadTimeout => e
|
84
|
+
if wait
|
85
|
+
retry
|
86
|
+
else
|
87
|
+
raise e
|
88
|
+
end
|
80
89
|
end
|
81
90
|
|
82
91
|
def next
|
@@ -123,40 +132,66 @@ module DACPClient
|
|
123
132
|
end
|
124
133
|
|
125
134
|
def queue(id)
|
126
|
-
do_action
|
135
|
+
do_action('playqueue-edit', { command: 'add',
|
136
|
+
query: "\'dmap.itemid:#{id}\'" })
|
127
137
|
end
|
128
138
|
|
129
139
|
def clear_queue
|
130
|
-
do_action
|
140
|
+
do_action('playqueue-edit', { command: 'clear' })
|
131
141
|
end
|
132
142
|
|
133
143
|
def list_queue
|
134
|
-
do_action
|
144
|
+
do_action('playqueue-contents', {})
|
135
145
|
end
|
136
146
|
|
137
147
|
def databases
|
138
|
-
do_action
|
148
|
+
do_action('databases', {}, true)
|
139
149
|
end
|
140
150
|
|
141
151
|
def playlists(db)
|
142
|
-
do_action
|
152
|
+
do_action("databases/#{db}/containers", {}, true)
|
143
153
|
end
|
144
154
|
|
145
|
-
def
|
146
|
-
|
155
|
+
def default_db
|
156
|
+
databases[:mlcl].to_a.find {|item| item.mdbk == 1}
|
147
157
|
end
|
148
158
|
|
149
|
-
def
|
150
|
-
|
159
|
+
def default_playlist(db)
|
160
|
+
@client.playlists(72).mlcl.to_a.find { |item| item.abpl }
|
151
161
|
end
|
152
162
|
|
153
|
-
def
|
154
|
-
|
163
|
+
def artwork(database, id, width = 320, height = 320)
|
164
|
+
url = "databases/#{database}/items/#{id}/extra_data/artwork"
|
165
|
+
do_action(url, { mw: width, mh: height }, true)
|
166
|
+
end
|
167
|
+
|
168
|
+
def now_playing_artwork(width = 320, height = 320)
|
169
|
+
do_action(:nowplayingartwork, { mw: width, mh: height })
|
170
|
+
end
|
171
|
+
|
172
|
+
def search(db, container, search, type = nil)
|
173
|
+
search = URI.escape(search)
|
174
|
+
types = {
|
175
|
+
title: 'dmap.itemname',
|
176
|
+
artist: 'daap.songartist',
|
177
|
+
album: 'daap.songalbum',
|
178
|
+
genre: 'daap.songgenre',
|
179
|
+
composer: 'daap.songcomposer'
|
180
|
+
}
|
155
181
|
queries = []
|
156
|
-
|
182
|
+
type = types.keys if type.nil?
|
183
|
+
Array(type).each do |t|
|
184
|
+
queries << "'#{types[t]}:#{search}'"
|
185
|
+
end
|
186
|
+
# @http.get("/databases/1/containers/1/items?query='daap.songartist:#{escaped_pattern}','daap.songalbum:#{escaped_pattern}','dmap.itemname:#{escaped_pattern}','daap.songgenre:#{escaped_pattern}','daap.songcomposer:#{escaped_pattern}'").body
|
187
|
+
#queries.push(words.map { |v| "\'dmap.itemname:*#{v}*\'" }.join('+'))
|
157
188
|
# queries.push(words.map{|v| "\'daap.songartist:*#{v}*\'"}.join('+'))
|
158
|
-
|
159
|
-
|
189
|
+
q = queries.join(',')
|
190
|
+
meta = 'dmap.itemname,dmap.itemid,daap.songartist,daap.songalbumartist,daap.songalbum,com.apple.itunes.cloud-id,dmap.containeritemid,com.apple.itunes.has-video,com.apple.itunes.itms-songid,com.apple.itunes.extended-media-kind,dmap.downloadstatus,daap.songdisabled'
|
191
|
+
|
192
|
+
url = "databases/#{db}/containers/#{container}/items"
|
193
|
+
do_action(url, { type: 'music', sort: 'album', query: q, meta: meta},
|
194
|
+
true)
|
160
195
|
end
|
161
196
|
|
162
197
|
private
|
@@ -167,26 +202,34 @@ module DACPClient
|
|
167
202
|
params['session-id'] = @session_id
|
168
203
|
action = '/ctrl-int/1' + action unless cleanurl
|
169
204
|
end
|
170
|
-
params = params.map { |k,
|
171
|
-
uri = URI::HTTP.build({ host: @host, port: @port, path: action,
|
205
|
+
params = params.map { |k,v| "#{k}=#{v}" }.join('&')
|
206
|
+
uri = URI::HTTP.build({ host: @host, port: @port, path: action,
|
207
|
+
query: params })
|
172
208
|
req = Net::HTTP::Get.new(uri.request_uri)
|
173
209
|
req.add_field('Viewer-Only-Client', '1')
|
174
|
-
res = Net::HTTP.new(uri.host, uri.port).start
|
175
|
-
|
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)
|
176
216
|
raise DACPForbiddenError.new(res)
|
177
217
|
elsif !res.kind_of?(Net::HTTPSuccess)
|
178
|
-
|
218
|
+
warn 'No succes!'
|
219
|
+
warn res
|
179
220
|
return nil
|
180
221
|
end
|
181
222
|
|
182
223
|
if res['Content-Type'] == 'application/x-dmap-tagged'
|
183
|
-
DMAPParser.parse(res.body)
|
224
|
+
DMAPParser::Parser.parse(res.body)
|
184
225
|
else
|
185
226
|
res.body
|
186
227
|
end
|
187
228
|
end
|
188
229
|
end
|
189
230
|
|
231
|
+
# This error is raised if the DACP resource returns forbidden or
|
232
|
+
# service unavailable
|
190
233
|
class DACPForbiddenError < StandardError
|
191
234
|
attr_reader :result
|
192
235
|
def initialize(res)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
module DACPClient
|
2
|
+
# This class provides a DSL to create DMAP responses
|
2
3
|
class DMAPBuilder
|
3
4
|
attr_reader :result
|
4
5
|
|
@@ -7,11 +8,14 @@ module DACPClient
|
|
7
8
|
end
|
8
9
|
|
9
10
|
def self.method_missing(method, *args, &block)
|
10
|
-
|
11
|
+
new.send(method, *args, &block)
|
11
12
|
end
|
12
13
|
|
13
14
|
def method_missing(method, *args, &block)
|
14
|
-
|
15
|
+
if method.to_s.length != 4 ||
|
16
|
+
(tag = DMAPParser::Types.find { |a| a.tag.to_s == method.to_s }).nil?
|
17
|
+
return super
|
18
|
+
end
|
15
19
|
if block_given?
|
16
20
|
if tag.type == :container
|
17
21
|
@dmap_stack << DMAPParser::TagContainer.new(tag)
|
@@ -26,7 +30,8 @@ module DACPClient
|
|
26
30
|
end
|
27
31
|
else
|
28
32
|
if @dmap_stack.length > 0
|
29
|
-
|
33
|
+
args = args.size > 1 ? args : args.first
|
34
|
+
@dmap_stack.last.value << DMAPParser::Tag.new(tag, args)
|
30
35
|
else
|
31
36
|
raise 'Cannot build DMAP without a valid container'
|
32
37
|
end
|
@@ -1,82 +1,86 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
require 'dacpclient/tagdefinitions'
|
2
|
+
require 'dacpclient/dmapconverter'
|
3
3
|
|
4
4
|
require 'stringio'
|
5
5
|
module DACPClient
|
6
|
-
|
6
|
+
module DMAPParser
|
7
|
+
# The DMAPParser class parses DMAP responses
|
8
|
+
class Parser
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
def self.parse(response)
|
11
|
+
return nil if response.nil? || response.length < 8
|
12
|
+
response = StringIO.new(response)
|
13
|
+
ret = TagContainer.new
|
14
|
+
key = response.read(4)
|
15
|
+
ret.type = Types.find { |a| a.tag == key }
|
16
|
+
response.read(4) # ignore length for now
|
17
|
+
ret.value = parse_container(response)
|
18
|
+
ret
|
19
|
+
end
|
18
20
|
|
19
|
-
|
21
|
+
private
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
+
def self.parse_container(response)
|
24
|
+
values = []
|
23
25
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
26
|
+
until response.eof?
|
27
|
+
key = response.read(4)
|
28
|
+
length = DMAPConverter.bin_to_int(response.read(4))
|
29
|
+
data = response.read(length)
|
30
|
+
tag = Types.find { |a| a.tag.to_s == key }
|
31
|
+
# puts "#{key} (#{length}): #{data.inspect}"
|
32
|
+
p data if !tag.nil? && tag.tag.to_s == 'msas'
|
33
|
+
values << if !tag.nil?
|
34
|
+
case tag.type
|
35
|
+
when :container
|
36
|
+
data = StringIO.new(data)
|
37
|
+
TagContainer.new(tag, parse_container(data))
|
38
|
+
when :byte
|
39
|
+
Tag.new(tag, DMAPConverter.bin_to_byte(data))
|
40
|
+
when :uint16, :short
|
41
|
+
Tag.new(tag, DMAPConverter.bin_to_short(data))
|
42
|
+
when :uint32
|
43
|
+
Tag.new(tag, DMAPConverter.bin_to_int(data))
|
44
|
+
when :uint64
|
45
|
+
Tag.new(tag, DMAPConverter.bin_to_long(data))
|
46
|
+
when :bool
|
47
|
+
Tag.new(tag, DMAPConverter.bin_to_bool(data))
|
48
|
+
when :hex
|
49
|
+
Tag.new(tag, DMAPConverter.bin_to_hex(data))
|
50
|
+
when :string
|
51
|
+
Tag.new(tag, data)
|
52
|
+
when :date
|
53
|
+
Tag.new tag, Time.at(DMAPConverter.bin_to_int(data))
|
54
|
+
when :version
|
55
|
+
Tag.new tag, DMAPConverter.bin_to_version(data)
|
56
|
+
else
|
57
|
+
warn "Unknown type #{tag.type}"
|
58
|
+
Tag.new(tag, parseunknown(data))
|
59
|
+
end
|
60
|
+
else
|
61
|
+
# puts "Unknown key #{key}"
|
62
|
+
tag = TagDefinition.new(key, :unknown,
|
63
|
+
"unknown (#{data.bytesize})")
|
64
|
+
Tag.new(tag, parseunknown(data))
|
65
|
+
end
|
60
66
|
end
|
67
|
+
values
|
61
68
|
end
|
62
|
-
values
|
63
|
-
end
|
64
69
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
70
|
+
def self.parseunknown(data)
|
71
|
+
if data =~ /[^\x20-\x7e]/ # non-readable characters
|
72
|
+
if data.bytesize == 1
|
73
|
+
return DMAPConverter.bin_to_byte(data)
|
74
|
+
elsif data.bytesize == 2
|
75
|
+
return DMAPConverter.bin_to_short(data)
|
76
|
+
elsif data.bytesize == 4
|
77
|
+
return DMAPConverter.bin_to_int(data)
|
78
|
+
elsif data.bytesize == 8
|
79
|
+
return DMAPConverter.bin_to_long(data)
|
80
|
+
end
|
75
81
|
end
|
76
|
-
else
|
77
82
|
data
|
78
83
|
end
|
79
84
|
end
|
80
|
-
|
81
85
|
end
|
82
86
|
end
|
@@ -2,6 +2,7 @@ require 'socket'
|
|
2
2
|
require 'dnssd'
|
3
3
|
require 'digest'
|
4
4
|
module DACPClient
|
5
|
+
# The pairingserver handles pairing with iTunes
|
5
6
|
class PairingServer
|
6
7
|
attr_accessor :pin, :device_type
|
7
8
|
def initialize(name, host, port = 1024)
|
@@ -14,40 +15,29 @@ module DACPClient
|
|
14
15
|
end
|
15
16
|
|
16
17
|
def start
|
17
|
-
|
18
|
-
pair = @pair
|
19
|
-
device_type = @device_type
|
20
|
-
puts "Pairing started (pincode=#{@pin.join})"
|
21
|
-
txtrecord = DNSSD::TextRecord.new({
|
22
|
-
'DvNm' => @name,
|
23
|
-
'Revm' => '10000',
|
24
|
-
'DvTy' => @device_type,
|
25
|
-
'RemN' => 'Remote',
|
26
|
-
'txtvers' => '1',
|
27
|
-
'Pair' => @pair
|
28
|
-
})
|
18
|
+
# puts "Pairing started (pincode=#{@pin.join})"
|
29
19
|
|
30
|
-
|
31
|
-
cmpg pair
|
32
|
-
cmnm name
|
33
|
-
cmty device_type
|
34
|
-
end.to_dmap
|
20
|
+
pairing_string = generate_pairing_string(@pair, @name, @device_type)
|
35
21
|
|
36
22
|
expected = PairingServer.generate_pin_challenge(@pair, @pin)
|
37
23
|
server = TCPServer.open(@host, @port)
|
38
|
-
|
24
|
+
type = '_touch-remote._tcp'
|
25
|
+
@service = DNSSD.register!(@name, type, 'local', @port, text_record)
|
39
26
|
|
40
27
|
while (client = server.accept)
|
41
28
|
get = client.gets
|
42
29
|
code = get.match(/pairingcode=([^&]*)/)[1]
|
43
30
|
|
44
31
|
if code == expected
|
45
|
-
client.print "HTTP/1.1 200 OK\r\
|
46
|
-
|
32
|
+
client.print "HTTP/1.1 200 OK\r\n"
|
33
|
+
client.print "Content-Length: #{pairing_string.length}\r\n\r\n"
|
34
|
+
client.print pairing_string
|
35
|
+
# puts 'Pairing succeeded :)'
|
47
36
|
client.close
|
37
|
+
@service.stop
|
48
38
|
break
|
49
39
|
else
|
50
|
-
puts 'Wrong pincode entered'
|
40
|
+
# puts 'Wrong pincode entered'
|
51
41
|
client.print "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
|
52
42
|
end
|
53
43
|
client.close
|
@@ -62,5 +52,25 @@ module DACPClient
|
|
62
52
|
Digest::MD5.hexdigest(pair + pin_string).upcase
|
63
53
|
end
|
64
54
|
|
55
|
+
private
|
56
|
+
|
57
|
+
def text_record
|
58
|
+
DNSSD::TextRecord.new({
|
59
|
+
'DvNm' => @name,
|
60
|
+
'Revm' => '10000',
|
61
|
+
'DvTy' => @device_type,
|
62
|
+
'RemN' => 'Remote',
|
63
|
+
'txtvers' => '1',
|
64
|
+
'Pair' => @pair
|
65
|
+
})
|
66
|
+
end
|
67
|
+
|
68
|
+
def generate_pairing_string(pair, name, device_type)
|
69
|
+
DMAPBuilder.cmpa do
|
70
|
+
cmpg pair
|
71
|
+
cmnm name
|
72
|
+
cmty device_type
|
73
|
+
end.to_dmap
|
74
|
+
end
|
65
75
|
end
|
66
76
|
end
|
@@ -1,7 +1,5 @@
|
|
1
|
-
# encoding: UTF-8
|
2
|
-
|
3
1
|
module DACPClient
|
4
|
-
|
2
|
+
module DMAPParser
|
5
3
|
|
6
4
|
Tag = Struct.new(:type, :value) do
|
7
5
|
def inspect(level = 0)
|
@@ -32,7 +30,7 @@ module DACPClient
|
|
32
30
|
when :version
|
33
31
|
DMAPConverter.version_to_bin value.to_i
|
34
32
|
else
|
35
|
-
|
33
|
+
warn "Unknown type #{tag.type}"
|
36
34
|
# Tag.new tag, parseunknown(data)
|
37
35
|
value
|
38
36
|
end
|
@@ -40,23 +38,29 @@ module DACPClient
|
|
40
38
|
end
|
41
39
|
end
|
42
40
|
|
41
|
+
# The TagContainer class is a Tag which contains other Tags
|
43
42
|
class TagContainer < Tag
|
44
|
-
|
45
43
|
def initialize(type = nil, value = [])
|
46
44
|
super type, value
|
47
45
|
end
|
48
46
|
|
49
47
|
def inspect(level = 0)
|
50
|
-
"#{' ' * level}#{type}:\n" + value.reduce('')
|
48
|
+
"#{' ' * level}#{type}:\n" + value.reduce('') do |a, e|
|
49
|
+
a + e.inspect(level + 1).chomp + "\n"
|
50
|
+
end
|
51
51
|
end
|
52
52
|
|
53
53
|
def get_value(key)
|
54
|
+
if key.is_a? Fixnum
|
55
|
+
return value[key]
|
56
|
+
end
|
54
57
|
key = key.to_s
|
55
58
|
val = value.find { |e| e.type.tag == key }
|
56
59
|
val = value.find { |e| e.type.name == key } if val.nil?
|
60
|
+
|
57
61
|
if val.type.type == :container
|
58
62
|
val
|
59
|
-
|
63
|
+
elsif !val.nil?
|
60
64
|
val.value
|
61
65
|
end
|
62
66
|
end
|
@@ -71,10 +75,11 @@ module DACPClient
|
|
71
75
|
end
|
72
76
|
|
73
77
|
def method_missing(method, *arguments, &block)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
+
get_value(method.to_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_a
|
82
|
+
value
|
78
83
|
end
|
79
84
|
end
|
80
85
|
|
@@ -92,6 +97,7 @@ module DACPClient
|
|
92
97
|
# https://github.com/chendo/dmap-ng/blob/master/lib/dmap/tag_definitions.rb
|
93
98
|
# https://code.google.com/p/ytrack/wiki/DMAP
|
94
99
|
# https://code.google.com/p/tunesremote-se/wiki/ContentCodes
|
100
|
+
# https://raw.github.com/mattstevens/dmap-parser/master/dmap_parser.c
|
95
101
|
# /content-codes
|
96
102
|
Types = [
|
97
103
|
TagDefinition.new('mcon', :container, 'dmap.container'),
|
@@ -136,7 +142,7 @@ module DACPClient
|
|
136
142
|
TagDefinition.new('asgn', :string, 'daap.songgenre'),
|
137
143
|
TagDefinition.new('asdt', :string, 'daap.songdescription'),
|
138
144
|
TagDefinition.new('asul', :string, 'daap.songdataurl'),
|
139
|
-
TagDefinition.new('ceWM', :string, 'com.apple.itunes.welcome-message'),
|
145
|
+
TagDefinition.new('ceWM', :string, 'com.apple.itunes.welcome-message'),
|
140
146
|
TagDefinition.new('ascp', :string, 'daap.songcomposer'),
|
141
147
|
TagDefinition.new('assu', :string, 'daap.sortartist'),
|
142
148
|
TagDefinition.new('assa', :string, 'daap.sortalbum'),
|
@@ -201,7 +207,7 @@ module DACPClient
|
|
201
207
|
TagDefinition.new('muty', :byte, 'dmap.updatetype'),
|
202
208
|
TagDefinition.new("f\215ch", :byte, 'dmap.haschildcontainers'),
|
203
209
|
TagDefinition.new('msas', :byte, 'dmap.authenticationschemes'),
|
204
|
-
TagDefinition.new('cavs', :bool, 'dacp.visualizer'),
|
210
|
+
TagDefinition.new('cavs', :bool, 'dacp.visualizer'),
|
205
211
|
TagDefinition.new('cafs', :bool, 'dacp.fullscreen'),
|
206
212
|
TagDefinition.new('ceGS', :bool, 'com.apple.itunes.genius-selectable'),
|
207
213
|
TagDefinition.new('mslr', :bool, 'dmap.loginrequired'),
|
@@ -223,8 +229,9 @@ module DACPClient
|
|
223
229
|
TagDefinition.new('aeFP', :bool, 'com.apple.itunes.req-fplay'),
|
224
230
|
TagDefinition.new('aeHV', :bool, 'com.apple.itunes.has-video'),
|
225
231
|
TagDefinition.new('caia', :bool, 'dacp.isavailiable'),
|
226
|
-
TagDefinition.new('ceVO', :bool, 'com.apple.itunes.voting-enabled'),
|
227
|
-
TagDefinition.new('aeSV', :version,
|
232
|
+
TagDefinition.new('ceVO', :bool, 'com.apple.itunes.voting-enabled'),
|
233
|
+
TagDefinition.new('aeSV', :version,
|
234
|
+
'com.apple.itunes.music-sharing-version'),
|
228
235
|
TagDefinition.new('mpro', :version, 'dmap.protocolversion'),
|
229
236
|
TagDefinition.new('apro', :version, 'daap.protocolversion'),
|
230
237
|
TagDefinition.new('musr', :version, 'dmap.serverrevision'),
|
@@ -235,9 +242,11 @@ module DACPClient
|
|
235
242
|
TagDefinition.new('ceJI', :bool, 'com.apple.itunes.jukebox-current'),
|
236
243
|
TagDefinition.new('ceJS', :uint16, 'com.apple.itunes.jukebox-score'),
|
237
244
|
TagDefinition.new('ceJV', :bool, 'com.apple.itunes.jukebox-vote'),
|
238
|
-
TagDefinition.new('ceQR', :container,
|
239
|
-
|
240
|
-
TagDefinition.new('
|
245
|
+
TagDefinition.new('ceQR', :container,
|
246
|
+
'com.apple.itunes.playqueue-contents-response'),
|
247
|
+
TagDefinition.new('ceQS', :container,
|
248
|
+
'com.apple.itunes.playqueue-contents-???'),
|
249
|
+
TagDefinition.new('ceQs', :uint64, 'com.apple.itunes.playqueue-id'),
|
241
250
|
TagDefinition.new('ceQa', :string, 'com.apple.itunes.playqueue-album'),
|
242
251
|
TagDefinition.new('ceQg', :string, 'com.apple.itunes.playqueue-genre'),
|
243
252
|
TagDefinition.new('ceQn', :string, 'com.apple.itunes.playqueue-name'),
|
@@ -246,6 +255,9 @@ module DACPClient
|
|
246
255
|
TagDefinition.new('aeGs', :bool, 'com.apple.itunes.can-be-genius-seed'),
|
247
256
|
TagDefinition.new('aprm', :short, 'daap.playlistrepeatmode'),
|
248
257
|
TagDefinition.new('apsm', :short, 'daap.playlistshufflemode'),
|
258
|
+
TagDefinition.new('cmpr', :version, 'dmcp.protocolversion'),
|
259
|
+
TagDefinition.new('capr', :version, 'dacp.protocolversion'),
|
260
|
+
TagDefinition.new('ppro', :version, 'unknown.version'),
|
249
261
|
|
250
262
|
].freeze
|
251
263
|
end
|
data/lib/dacpclient/version.rb
CHANGED
data/lib/dacpclient.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dacpclient
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jurriaan Pruis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-08-
|
11
|
+
date: 2013-08-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dnssd
|
@@ -66,23 +66,23 @@ dependencies:
|
|
66
66
|
- - '>='
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
description: A DACP (iTunes Remote protocol) client
|
70
|
-
language
|
69
|
+
description: A DACP (iTunes Remote protocol) client
|
71
70
|
email:
|
72
71
|
- email@jurriaanpruis.nl
|
73
|
-
executables:
|
72
|
+
executables:
|
73
|
+
- dacpclient
|
74
74
|
extensions: []
|
75
75
|
extra_rdoc_files:
|
76
76
|
- README.md
|
77
77
|
- LICENSE
|
78
78
|
files:
|
79
|
+
- .gitignore
|
79
80
|
- .rubocop.yml
|
80
81
|
- Gemfile
|
81
|
-
- Gemfile.lock
|
82
82
|
- LICENSE
|
83
83
|
- README.md
|
84
84
|
- Rakefile
|
85
|
-
-
|
85
|
+
- bin/dacpclient
|
86
86
|
- dacpclient.gemspec
|
87
87
|
- lib/dacpclient.rb
|
88
88
|
- lib/dacpclient/client.rb
|
@@ -92,7 +92,6 @@ files:
|
|
92
92
|
- lib/dacpclient/pairingserver.rb
|
93
93
|
- lib/dacpclient/tagdefinitions.rb
|
94
94
|
- lib/dacpclient/version.rb
|
95
|
-
- remoteclient.rb
|
96
95
|
homepage: https://github.com/jurriaan/ruby-dacpclient
|
97
96
|
licenses:
|
98
97
|
- MIT
|
@@ -105,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
104
|
requirements:
|
106
105
|
- - '>='
|
107
106
|
- !ruby/object:Gem::Version
|
108
|
-
version:
|
107
|
+
version: 1.9.3
|
109
108
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
109
|
requirements:
|
111
110
|
- - '>='
|
@@ -116,6 +115,6 @@ rubyforge_project:
|
|
116
115
|
rubygems_version: 2.0.5
|
117
116
|
signing_key:
|
118
117
|
specification_version: 4
|
119
|
-
summary: A DACP (iTunes Remote protocol) client
|
118
|
+
summary: A DACP (iTunes Remote protocol) client
|
120
119
|
test_files: []
|
121
120
|
has_rdoc:
|
data/Gemfile.lock
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
GIT
|
2
|
-
remote: git://github.com/bbatsov/rubocop.git
|
3
|
-
revision: 68b73d5c38ad86f8702302d440437dbd77f677c0
|
4
|
-
ref: 68b73d5c38a
|
5
|
-
specs:
|
6
|
-
rubocop (0.11.0)
|
7
|
-
backports (~> 3.3.3)
|
8
|
-
parser (~> 2.0.0.pre6)
|
9
|
-
powerpack (= 0.0.1)
|
10
|
-
rainbow (>= 1.1.4)
|
11
|
-
|
12
|
-
PATH
|
13
|
-
remote: .
|
14
|
-
specs:
|
15
|
-
dacpclient (0.1.0)
|
16
|
-
dnssd (~> 2.0)
|
17
|
-
|
18
|
-
GEM
|
19
|
-
remote: https://rubygems.org/
|
20
|
-
specs:
|
21
|
-
ast (1.1.0)
|
22
|
-
backports (3.3.3)
|
23
|
-
coderay (1.0.9)
|
24
|
-
dnssd (2.0)
|
25
|
-
github-markup (0.7.5)
|
26
|
-
method_source (0.8.2)
|
27
|
-
minitest (5.0.6)
|
28
|
-
multi_json (1.7.8)
|
29
|
-
parser (2.0.0.pre6)
|
30
|
-
ast (~> 1.1)
|
31
|
-
slop (~> 3.4, >= 3.4.5)
|
32
|
-
powerpack (0.0.1)
|
33
|
-
pry (0.9.12.2)
|
34
|
-
coderay (~> 1.0.5)
|
35
|
-
method_source (~> 0.8)
|
36
|
-
slop (~> 3.4)
|
37
|
-
rainbow (1.1.4)
|
38
|
-
rake (10.1.0)
|
39
|
-
redcarpet (3.0.0)
|
40
|
-
simplecov (0.7.1)
|
41
|
-
multi_json (~> 1.0)
|
42
|
-
simplecov-html (~> 0.7.1)
|
43
|
-
simplecov-html (0.7.1)
|
44
|
-
slop (3.4.6)
|
45
|
-
yard (0.8.7)
|
46
|
-
|
47
|
-
PLATFORMS
|
48
|
-
ruby
|
49
|
-
|
50
|
-
DEPENDENCIES
|
51
|
-
dacpclient!
|
52
|
-
github-markup
|
53
|
-
minitest (~> 5.0.6)
|
54
|
-
pry
|
55
|
-
rake
|
56
|
-
redcarpet
|
57
|
-
rubocop!
|
58
|
-
simplecov
|
59
|
-
yard
|