dacpclient 0.1.0 → 0.1.1
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/.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
|
+
[](http://badge.fury.io/rb/dacpclient) [](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
|