dacpclient 0.1.0
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 +7 -0
- data/.rubocop.yml +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +24 -0
- data/README.md +3 -0
- data/Rakefile +22 -0
- data/TODO +3 -0
- data/dacpclient.gemspec +28 -0
- data/lib/dacpclient/client.rb +196 -0
- data/lib/dacpclient/dmapbuilder.rb +36 -0
- data/lib/dacpclient/dmapconverter.rb +65 -0
- data/lib/dacpclient/dmapparser.rb +82 -0
- data/lib/dacpclient/pairingserver.rb +66 -0
- data/lib/dacpclient/tagdefinitions.rb +252 -0
- data/lib/dacpclient/version.rb +3 -0
- data/lib/dacpclient.rb +2 -0
- data/remoteclient.rb +126 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 851490abf8148566e8348090625db3b72d2d6328
|
|
4
|
+
data.tar.gz: c845ff13c853f52927eea02ead5465469a461f1e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 528948172ba3b6bd76e0cb3cc4ccf15508df58a86267035e65a07b2e489fa53cbcfe57cc868f4641bb18d3921878a04edb61b523f1fc9b01761cca563cc0320b
|
|
7
|
+
data.tar.gz: 1530e1c99b1d139467fd9638c617fa1fd95f4793abff03a89cad81a07052fa014a7ced470df0a12328663e3d5fd2d96aacbeaedf71123a75fabaa7a1d41a71f9
|
data/.rubocop.yml
ADDED
data/Gemfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ruby '2.0.0'
|
|
2
|
+
gemspec
|
|
3
|
+
|
|
4
|
+
source 'https://rubygems.org'
|
|
5
|
+
|
|
6
|
+
group :test do
|
|
7
|
+
gem 'pry'
|
|
8
|
+
gem 'minitest', '~> 5.0.6'
|
|
9
|
+
#gem 'coveralls', require: false
|
|
10
|
+
gem 'rake'
|
|
11
|
+
gem 'rubocop', github: 'bbatsov/rubocop', ref: '68b73d5c38a' #'~> 0.12.0'
|
|
12
|
+
gem 'simplecov', require: false
|
|
13
|
+
#gem 'simple_mock'
|
|
14
|
+
end
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
data/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Copyright (c) 2011-2013, Jurriaan Pruis <email@jurriaanpruis.nl>
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
* Redistributions of source code must retain the above copyright
|
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
* Neither the name of the <organization> nor the
|
|
12
|
+
names of its contributors may be used to endorse or promote products
|
|
13
|
+
derived from this software without specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL Jurriaan Pruis BE LIABLE FOR ANY
|
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'bundler/gem_tasks'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rubocop'
|
|
4
|
+
require 'yard'
|
|
5
|
+
YARD::Rake::YardocTask.new
|
|
6
|
+
|
|
7
|
+
# Rake::TestTask.new do |t|
|
|
8
|
+
# t.libs << 'lib/docparser'
|
|
9
|
+
# t.test_files = FileList['test/lib/**/*_test.rb']
|
|
10
|
+
# t.verbose = true
|
|
11
|
+
# end
|
|
12
|
+
|
|
13
|
+
task test: :rubocop
|
|
14
|
+
|
|
15
|
+
task :rubocop do
|
|
16
|
+
puts "Running Rubocop #{Rubocop::Version::STRING}"
|
|
17
|
+
args = FileList['**/*.rb', 'Rakefile', 'dacpclient.gemspec']
|
|
18
|
+
cli = Rubocop::CLI.new
|
|
19
|
+
fail unless cli.run(args) == 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
task default: :test
|
data/TODO
ADDED
data/dacpclient.gemspec
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
3
|
+
require 'dacpclient/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'dacpclient'
|
|
7
|
+
spec.version = DACPClient::VERSION
|
|
8
|
+
spec.authors = ['Jurriaan Pruis']
|
|
9
|
+
spec.email = ['email@jurriaanpruis.nl']
|
|
10
|
+
spec.description = 'A DACP (iTunes Remote protocol) client written in the wonderful Ruby language'
|
|
11
|
+
spec.summary = 'A DACP (iTunes Remote protocol) client written in the wonderful Ruby language'
|
|
12
|
+
spec.homepage = 'https://github.com/jurriaan/ruby-dacpclient'
|
|
13
|
+
spec.license = 'MIT'
|
|
14
|
+
spec.platform = Gem::Platform::RUBY
|
|
15
|
+
|
|
16
|
+
spec.files = `git ls-files`.split($RS)
|
|
17
|
+
spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
|
|
18
|
+
spec.test_files = spec.files.grep(/^(test|spec|features)\//)
|
|
19
|
+
spec.require_paths = ['lib']
|
|
20
|
+
spec.extra_rdoc_files = ['README.md', 'LICENSE']
|
|
21
|
+
|
|
22
|
+
spec.add_runtime_dependency 'dnssd', '~> 2.0'
|
|
23
|
+
|
|
24
|
+
spec.add_development_dependency 'yard'
|
|
25
|
+
spec.add_development_dependency 'redcarpet'
|
|
26
|
+
spec.add_development_dependency 'github-markup'
|
|
27
|
+
spec.required_ruby_version = '>= 2.0.0'
|
|
28
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
$LOAD_PATH.unshift __dir__
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'bundler/setup'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require_relative 'pairingserver'
|
|
7
|
+
require_relative 'dmapparser'
|
|
8
|
+
require_relative 'dmapbuilder'
|
|
9
|
+
require 'uri'
|
|
10
|
+
require 'cgi'
|
|
11
|
+
|
|
12
|
+
module DACPClient
|
|
13
|
+
class Client
|
|
14
|
+
|
|
15
|
+
def initialize(name, host = 'localhost', port = 3689)
|
|
16
|
+
@client = Net::HTTP.new(host, port)
|
|
17
|
+
@name = name
|
|
18
|
+
@host = host
|
|
19
|
+
@port = port
|
|
20
|
+
@service = nil
|
|
21
|
+
@session_id = nil
|
|
22
|
+
@mediarevision = 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pair(pin = nil)
|
|
26
|
+
pairingserver = PairingServer.new(@name, '0.0.0.0', 1024)
|
|
27
|
+
pairingserver.pin = pin unless pin.nil?
|
|
28
|
+
pairingserver.start
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.get_guid(name)
|
|
32
|
+
d = Digest::SHA2.hexdigest(name)
|
|
33
|
+
d[0..15]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serverinfo
|
|
37
|
+
do_action('server-info')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def login(pin = nil)
|
|
41
|
+
response = do_action(:login, { 'pairing-guid' => '0x' + Client.get_guid(@name) })
|
|
42
|
+
@session_id = response[:mlid]
|
|
43
|
+
response
|
|
44
|
+
rescue DACPForbiddenError => e
|
|
45
|
+
puts "#{e.result.message} error: Cannot login, starting pairing process"
|
|
46
|
+
pin = 4.times.map { Random.rand(10) } if pin.nil?
|
|
47
|
+
pair(pin)
|
|
48
|
+
retry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def content_codes
|
|
52
|
+
do_action('content-codes', {}, true)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def play
|
|
56
|
+
do_action(:play)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def playpause
|
|
60
|
+
do_action(:playpause)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def stop
|
|
64
|
+
do_action(:stop)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def pause
|
|
68
|
+
do_action(:pause)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def seek(ms)
|
|
72
|
+
do_action(:setproperty, 'dacp.playingtime' => ms)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def status(wait = false)
|
|
76
|
+
revision = wait ? @mediarevision : 1
|
|
77
|
+
result = do_action(:playstatusupdate, 'revision-number' => revision)
|
|
78
|
+
@mediarevision = result[:cmsr]
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def next
|
|
83
|
+
do_action(:nextitem)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def prev
|
|
87
|
+
do_action(:previtem)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def get_volume
|
|
91
|
+
response = do_action(:getproperty, properties: 'dmcp.volume')
|
|
92
|
+
response[:cmvo]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def set_volume(volume)
|
|
96
|
+
do_action(:setproperty, 'dmcp.volume' => volume)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def get_repeat
|
|
100
|
+
response = do_action(:getproperty, properties: 'dacp.repeatstate')
|
|
101
|
+
response[:carp]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def set_repeat(volume)
|
|
105
|
+
do_action(:setproperty, 'dmcp.volume' => volume)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def get_shuffle
|
|
109
|
+
response = do_action(:getproperty, properties: 'dmcp.shufflestate')
|
|
110
|
+
response[:cash]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def set_shuffle(volume)
|
|
114
|
+
do_action(:setproperty, 'dmcp.volume' => volume)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def ctrl_int
|
|
118
|
+
do_action('ctrl-int', {}, false)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def logout
|
|
122
|
+
do_action(:logout, {}, false)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def queue(id)
|
|
126
|
+
do_action 'playqueue-edit', { command: 'add', query: "\'dmap.itemid:#{id}\'" }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def clear_queue
|
|
130
|
+
do_action 'playqueue-edit', { command: 'clear' }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def list_queue
|
|
134
|
+
do_action 'playqueue-contents', {}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def databases
|
|
138
|
+
do_action 'databases', {}, true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def playlists(db)
|
|
142
|
+
do_action "databases/#{db}/containers", {}, true
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def artwork(database, id, width = 320, height = 320)
|
|
146
|
+
do_action "databases/#{database}/items/#{id}/extra_data/artwork", { mw: width, mh: height }, true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def now_playing_artwork(width = 320, height = 320)
|
|
150
|
+
do_action :nowplayingartwork, { mw: width, mh: height }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def search(db, container, search)
|
|
154
|
+
words = search.split
|
|
155
|
+
queries = []
|
|
156
|
+
queries.push(words.map { |v| "\'dmap.itemname:*#{v}*\'" }.join('+'))
|
|
157
|
+
# queries.push(words.map{|v| "\'daap.songartist:*#{v}*\'"}.join('+'))
|
|
158
|
+
query = '(' + queries.map { |q| "(#{q})" }.join(',') + ')'
|
|
159
|
+
do_action "databases/#{db}/containers/#{container}/items", { type: 'music', sort: 'album', meta: 'dmap.itemid,dmap.itemname,daap.songartist,daap.songalbum', query: query }, true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def do_action(action, params = {}, cleanurl = false)
|
|
165
|
+
action = '/' + action.to_s
|
|
166
|
+
unless @session_id.nil?
|
|
167
|
+
params['session-id'] = @session_id
|
|
168
|
+
action = '/ctrl-int/1' + action unless cleanurl
|
|
169
|
+
end
|
|
170
|
+
params = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join '&'
|
|
171
|
+
uri = URI::HTTP.build({ host: @host, port: @port, path: action, query: params })
|
|
172
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
173
|
+
req.add_field('Viewer-Only-Client', '1')
|
|
174
|
+
res = Net::HTTP.new(uri.host, uri.port).start { |http| http.request(req) }
|
|
175
|
+
if res.kind_of?(Net::HTTPServiceUnavailable) || res.kind_of?(Net::HTTPForbidden)
|
|
176
|
+
raise DACPForbiddenError.new(res)
|
|
177
|
+
elsif !res.kind_of?(Net::HTTPSuccess)
|
|
178
|
+
p res
|
|
179
|
+
return nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if res['Content-Type'] == 'application/x-dmap-tagged'
|
|
183
|
+
DMAPParser.parse(res.body)
|
|
184
|
+
else
|
|
185
|
+
res.body
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
class DACPForbiddenError < StandardError
|
|
191
|
+
attr_reader :result
|
|
192
|
+
def initialize(res)
|
|
193
|
+
@result = res
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module DACPClient
|
|
2
|
+
class DMAPBuilder
|
|
3
|
+
attr_reader :result
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@dmap_stack = []
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.method_missing(method, *args, &block)
|
|
10
|
+
self.new.send(method, *args, &block)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def method_missing(method, *args, &block)
|
|
14
|
+
return super if method.to_s.length != 4 || (tag = DMAPParser::Types.find { |a| a.tag.to_s == method.to_s }).nil?
|
|
15
|
+
if block_given?
|
|
16
|
+
if tag.type == :container
|
|
17
|
+
@dmap_stack << DMAPParser::TagContainer.new(tag)
|
|
18
|
+
instance_eval(&block)
|
|
19
|
+
if @dmap_stack.length > 1
|
|
20
|
+
@dmap_stack.last.value << @dmap_stack.pop
|
|
21
|
+
else
|
|
22
|
+
return @result = @dmap_stack.pop
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
raise "Tag #{method} is not a container type"
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
if @dmap_stack.length > 0
|
|
29
|
+
@dmap_stack.last.value << DMAPParser::Tag.new(tag, args.size > 1 ? args : args.first)
|
|
30
|
+
else
|
|
31
|
+
raise 'Cannot build DMAP without a valid container'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module DACPClient
|
|
2
|
+
class DMAPConverter
|
|
3
|
+
class << self
|
|
4
|
+
def bin_to_byte(data)
|
|
5
|
+
data.unpack('C').first
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def bin_to_long(data)
|
|
9
|
+
(bin_to_int(data[0..3]) << 32) + bin_to_int(data[4..7])
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def bin_to_int(data)
|
|
13
|
+
data.unpack('N').first
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def bin_to_short(data)
|
|
17
|
+
data.unpack('n').first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bin_to_bool(data)
|
|
21
|
+
data == "\x01"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bin_to_version(data)
|
|
25
|
+
data.unpack('nCC').join '.'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def bin_to_hex(data)
|
|
29
|
+
data.bytes.reduce('') { |a, e| a += sprintf('%02X', e) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def bool_to_bin(data)
|
|
33
|
+
if data.true?
|
|
34
|
+
"\x01"
|
|
35
|
+
else
|
|
36
|
+
"\x00"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def int_to_bin(data)
|
|
41
|
+
[data.to_i].pack 'N'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def byte_to_bin(data)
|
|
45
|
+
[data.to_i].pack 'C'
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def long_to_bin(data)
|
|
49
|
+
[data >> 32, data & 0xfffffff].pack 'NN'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def short_to_bin(data)
|
|
53
|
+
[data.to_i].pack 'n'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def version_to_bin(data)
|
|
57
|
+
data.split('.').pack 'nCC'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def hex_to_bin(data)
|
|
61
|
+
[data].pack 'H*'
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative 'tagdefinitions'
|
|
2
|
+
require_relative 'dmapconverter'
|
|
3
|
+
|
|
4
|
+
require 'stringio'
|
|
5
|
+
module DACPClient
|
|
6
|
+
class DMAPParser
|
|
7
|
+
|
|
8
|
+
def self.parse(response)
|
|
9
|
+
return nil if response.nil? || response.length < 8
|
|
10
|
+
response = StringIO.new(response)
|
|
11
|
+
ret = TagContainer.new
|
|
12
|
+
key = response.read(4)
|
|
13
|
+
ret.type = Types.find { |a| a.tag == key }
|
|
14
|
+
response.read(4) # ignore length for now
|
|
15
|
+
ret.value = parse_container(response)
|
|
16
|
+
ret
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def self.parse_container(response)
|
|
22
|
+
values = []
|
|
23
|
+
|
|
24
|
+
until response.eof?
|
|
25
|
+
key = response.read(4)
|
|
26
|
+
length = DMAPConverter.bin_to_int(response.read(4))
|
|
27
|
+
data = response.read(length)
|
|
28
|
+
tag = Types.find { |a| a.tag.to_s == key }
|
|
29
|
+
# puts "#{key} (#{length}): #{data.inspect}"
|
|
30
|
+
p data if !tag.nil? && tag.tag.to_s == 'msas'
|
|
31
|
+
values << if !tag.nil?
|
|
32
|
+
case tag.type
|
|
33
|
+
when :container
|
|
34
|
+
TagContainer.new(tag, parse_container(StringIO.new(data)))
|
|
35
|
+
when :byte
|
|
36
|
+
Tag.new(tag, DMAPConverter.bin_to_byte(data))
|
|
37
|
+
when :uint16, :short
|
|
38
|
+
Tag.new(tag, DMAPConverter.bin_to_short(data))
|
|
39
|
+
when :uint32
|
|
40
|
+
Tag.new(tag, DMAPConverter.bin_to_int(data))
|
|
41
|
+
when :uint64
|
|
42
|
+
Tag.new(tag, DMAPConverter.bin_to_long(data))
|
|
43
|
+
when :bool
|
|
44
|
+
Tag.new(tag, DMAPConverter.bin_to_bool(data))
|
|
45
|
+
when :hex
|
|
46
|
+
Tag.new(tag, DMAPConverter.bin_to_hex(data))
|
|
47
|
+
when :string
|
|
48
|
+
Tag.new(tag, data)
|
|
49
|
+
when :date
|
|
50
|
+
Tag.new tag, Time.at(DMAPConverter.bin_to_int(data))
|
|
51
|
+
when :version
|
|
52
|
+
Tag.new tag, DMAPConverter.bin_to_version(data)
|
|
53
|
+
else
|
|
54
|
+
puts "Unknown type #{tag.type}"
|
|
55
|
+
Tag.new(tag, parseunknown(data))
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
# puts "Unknown key #{key}"
|
|
59
|
+
Tag.new(TagDefinition.new(key, :unknown, "unknown (#{data.bytesize})"), parseunknown(data))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
values
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.parseunknown(data)
|
|
66
|
+
if data =~ /[^\x20-\x7e]/
|
|
67
|
+
if data.bytesize == 1
|
|
68
|
+
DMAPConverter.bin_to_byte(data)
|
|
69
|
+
elsif data.bytesize == 4
|
|
70
|
+
DMAPConverter.bin_to_int(data)
|
|
71
|
+
elsif data.bytesize == 8
|
|
72
|
+
DMAPConverter.bin_to_long(data)
|
|
73
|
+
else
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
data
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'dnssd'
|
|
3
|
+
require 'digest'
|
|
4
|
+
module DACPClient
|
|
5
|
+
class PairingServer
|
|
6
|
+
attr_accessor :pin, :device_type
|
|
7
|
+
def initialize(name, host, port = 1024)
|
|
8
|
+
@name = name
|
|
9
|
+
@port = port
|
|
10
|
+
@host = host
|
|
11
|
+
@pair = Client.get_guid(@name)
|
|
12
|
+
@pin = [0, 0, 0, 0]
|
|
13
|
+
@device_type = 'iPod'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start
|
|
17
|
+
name = @name
|
|
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
|
+
})
|
|
29
|
+
|
|
30
|
+
pairingstring = DMAPBuilder.cmpa do
|
|
31
|
+
cmpg pair
|
|
32
|
+
cmnm name
|
|
33
|
+
cmty device_type
|
|
34
|
+
end.to_dmap
|
|
35
|
+
|
|
36
|
+
expected = PairingServer.generate_pin_challenge(@pair, @pin)
|
|
37
|
+
server = TCPServer.open(@host, @port)
|
|
38
|
+
@service = DNSSD.register!(@name, '_touch-remote._tcp', 'local', @port, txtrecord)
|
|
39
|
+
|
|
40
|
+
while (client = server.accept)
|
|
41
|
+
get = client.gets
|
|
42
|
+
code = get.match(/pairingcode=([^&]*)/)[1]
|
|
43
|
+
|
|
44
|
+
if code == expected
|
|
45
|
+
client.print "HTTP/1.1 200 OK\r\nContent-Length: #{pairingstring.length}\r\n\r\n#{pairingstring}"
|
|
46
|
+
puts 'Pairing succeeded :)'
|
|
47
|
+
client.close
|
|
48
|
+
break
|
|
49
|
+
else
|
|
50
|
+
puts 'Wrong pincode entered'
|
|
51
|
+
client.print "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
|
|
52
|
+
end
|
|
53
|
+
client.close
|
|
54
|
+
end
|
|
55
|
+
server.close
|
|
56
|
+
|
|
57
|
+
sleep 1 # sleep so iTunes accepts our login
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.generate_pin_challenge(pair, pin)
|
|
61
|
+
pin_string = pin.map { |i| "#{i}\x00" }.join
|
|
62
|
+
Digest::MD5.hexdigest(pair + pin_string).upcase
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
module DACPClient
|
|
4
|
+
class DMAPParser
|
|
5
|
+
|
|
6
|
+
Tag = Struct.new(:type, :value) do
|
|
7
|
+
def inspect(level = 0)
|
|
8
|
+
"#{' ' * level}#{type}: #{value}"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_dmap
|
|
12
|
+
value = self.value
|
|
13
|
+
value = case type.type
|
|
14
|
+
when :container
|
|
15
|
+
value.reduce('') { |a, e| a += e.to_dmap }
|
|
16
|
+
when :byte
|
|
17
|
+
DMAPConverter.byte_to_bin value
|
|
18
|
+
when :uint16, :short
|
|
19
|
+
DMAPConverter.short_to_bin value
|
|
20
|
+
when :uint32
|
|
21
|
+
DMAPConverter.int_to_bin value
|
|
22
|
+
when :uint64
|
|
23
|
+
DMAPConverter.long_to_bin value
|
|
24
|
+
when :bool
|
|
25
|
+
DMAPConverter.bool_to_bin value
|
|
26
|
+
when :hex
|
|
27
|
+
DMAPConverter.hex_to_bin value
|
|
28
|
+
when :string
|
|
29
|
+
value
|
|
30
|
+
when :date
|
|
31
|
+
DMAPConverter.int_to_bin value.to_i
|
|
32
|
+
when :version
|
|
33
|
+
DMAPConverter.version_to_bin value.to_i
|
|
34
|
+
else
|
|
35
|
+
puts "Unknown type #{tag.type}"
|
|
36
|
+
# Tag.new tag, parseunknown(data)
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
type.tag.to_s + [value.length].pack('N') + value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class TagContainer < Tag
|
|
44
|
+
|
|
45
|
+
def initialize(type = nil, value = [])
|
|
46
|
+
super type, value
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inspect(level = 0)
|
|
50
|
+
"#{' ' * level}#{type}:\n" + value.reduce('') { |a, e| a + e.inspect(level + 1).chomp + "\n" }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def get_value(key)
|
|
54
|
+
key = key.to_s
|
|
55
|
+
val = value.find { |e| e.type.tag == key }
|
|
56
|
+
val = value.find { |e| e.type.name == key } if val.nil?
|
|
57
|
+
if val.type.type == :container
|
|
58
|
+
val
|
|
59
|
+
else
|
|
60
|
+
val.value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias_method :[], :get_value
|
|
65
|
+
|
|
66
|
+
def has?(key)
|
|
67
|
+
key = key.to_s
|
|
68
|
+
val = value.find { |e| e.type.tag == key }
|
|
69
|
+
val = value.find { |e| e.type.name == key } if val.nil?
|
|
70
|
+
!val.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def method_missing(method, *arguments, &block)
|
|
74
|
+
value.each do |dmap|
|
|
75
|
+
return dmap.value if dmap.type.tag.to_s == method.to_s
|
|
76
|
+
end
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
TagDefinition = Struct.new(:tag, :type, :name) do
|
|
82
|
+
def inspect
|
|
83
|
+
"#{tag} (#{name}: #{type})"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def to_s
|
|
87
|
+
"#{tag} (#{name}: #{type})"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Sources:
|
|
92
|
+
# https://github.com/chendo/dmap-ng/blob/master/lib/dmap/tag_definitions.rb
|
|
93
|
+
# https://code.google.com/p/ytrack/wiki/DMAP
|
|
94
|
+
# https://code.google.com/p/tunesremote-se/wiki/ContentCodes
|
|
95
|
+
# /content-codes
|
|
96
|
+
Types = [
|
|
97
|
+
TagDefinition.new('mcon', :container, 'dmap.container'),
|
|
98
|
+
TagDefinition.new('msrv', :container, 'dmap.serverinforesponse'),
|
|
99
|
+
TagDefinition.new('msml', :container, 'dmap.msml'),
|
|
100
|
+
TagDefinition.new('mccr', :container, 'dmap.contentcodesresponse'),
|
|
101
|
+
TagDefinition.new('mdcl', :container, 'dmap.dictionary'),
|
|
102
|
+
TagDefinition.new('mlog', :container, 'dmap.loginresponse'),
|
|
103
|
+
TagDefinition.new('mupd', :container, 'dmap.updateresponse'),
|
|
104
|
+
TagDefinition.new('avdb', :container, 'daap.serverdatabases'),
|
|
105
|
+
TagDefinition.new('mlcl', :container, 'dmap.listing'),
|
|
106
|
+
TagDefinition.new('mlit', :container, 'dmap.listingitem'),
|
|
107
|
+
TagDefinition.new('mbcl', :container, 'dmap.bag'),
|
|
108
|
+
TagDefinition.new('adbs', :container, 'daap.returndatabasesongs'),
|
|
109
|
+
TagDefinition.new('aply', :container, 'daap.databaseplaylists'),
|
|
110
|
+
TagDefinition.new('apso', :container, 'daap.playlistsongs'),
|
|
111
|
+
TagDefinition.new('mudl', :container, 'dmap.deletedidlisting'),
|
|
112
|
+
TagDefinition.new('abro', :container, 'daap.databasebrowse'),
|
|
113
|
+
TagDefinition.new('abal', :container, 'daap.browsealbumlisting'),
|
|
114
|
+
TagDefinition.new('abar', :container, 'daap.browseartistlisting'),
|
|
115
|
+
TagDefinition.new('abcp', :container, 'daap.browsecomposerlisting'),
|
|
116
|
+
TagDefinition.new('abgn', :container, 'daap.browsegenrelisting'),
|
|
117
|
+
TagDefinition.new('prsv', :container, 'daap.resolve'),
|
|
118
|
+
TagDefinition.new('arif', :container, 'daap.resolveinfo'),
|
|
119
|
+
TagDefinition.new('casp', :container, 'dacp.speakers'),
|
|
120
|
+
TagDefinition.new('caci', :container, 'dacp.controlint'),
|
|
121
|
+
TagDefinition.new('cmpa', :container, 'dacp.pairinganswer'),
|
|
122
|
+
TagDefinition.new('cacr', :container, 'dacp.cacr'),
|
|
123
|
+
TagDefinition.new('cmcp', :container, 'dmcp.controlprompt'),
|
|
124
|
+
TagDefinition.new('cmgt', :container, 'dmcp.getpropertyresponse'),
|
|
125
|
+
TagDefinition.new('cmst', :container, 'dmcp.status'),
|
|
126
|
+
TagDefinition.new('agal', :container, 'daap.albumgrouping'),
|
|
127
|
+
TagDefinition.new('minm', :string, 'dmap.itemname'),
|
|
128
|
+
TagDefinition.new('msts', :string, 'dmap.statusstring'),
|
|
129
|
+
TagDefinition.new('mcna', :string, 'dmap.contentcodesname'),
|
|
130
|
+
TagDefinition.new('asal', :string, 'daap.songalbum'),
|
|
131
|
+
TagDefinition.new('asaa', :string, 'daap.songalbumartist'),
|
|
132
|
+
TagDefinition.new('asar', :string, 'daap.songartist'),
|
|
133
|
+
TagDefinition.new('ascm', :string, 'daap.songcomment'),
|
|
134
|
+
TagDefinition.new('asfm', :string, 'daap.songformat'),
|
|
135
|
+
TagDefinition.new('aseq', :string, 'daap.songeqpreset'),
|
|
136
|
+
TagDefinition.new('asgn', :string, 'daap.songgenre'),
|
|
137
|
+
TagDefinition.new('asdt', :string, 'daap.songdescription'),
|
|
138
|
+
TagDefinition.new('asul', :string, 'daap.songdataurl'),
|
|
139
|
+
TagDefinition.new('ceWM', :string, 'com.apple.itunes.welcome-message'), # not a official name?
|
|
140
|
+
TagDefinition.new('ascp', :string, 'daap.songcomposer'),
|
|
141
|
+
TagDefinition.new('assu', :string, 'daap.sortartist'),
|
|
142
|
+
TagDefinition.new('assa', :string, 'daap.sortalbum'),
|
|
143
|
+
TagDefinition.new('agrp', :string, 'daap.songgrouping'),
|
|
144
|
+
TagDefinition.new('cann', :string, 'daap.nowplayingtrack'),
|
|
145
|
+
TagDefinition.new('cana', :string, 'daap.nowplayingartist'),
|
|
146
|
+
TagDefinition.new('canl', :string, 'daap.nowplayingalbum'),
|
|
147
|
+
TagDefinition.new('cang', :string, 'daap.nowplayinggenre'),
|
|
148
|
+
TagDefinition.new('cmnm', :string, 'dacp.devicename'),
|
|
149
|
+
TagDefinition.new('cmty', :string, 'dacp.devicetype'),
|
|
150
|
+
TagDefinition.new('cmpg', :hex, 'dacp.pairingguid'), # hex string
|
|
151
|
+
TagDefinition.new('mper', :uint64, 'dmap.persistentid'),
|
|
152
|
+
TagDefinition.new('canp', :uint64, 'dacp.nowplaying'),
|
|
153
|
+
TagDefinition.new('cmpy', :uint64, 'dacp.passguid'),
|
|
154
|
+
TagDefinition.new('mstt', :uint32, 'dmap.status'), # http status??
|
|
155
|
+
TagDefinition.new('mcnm', :uint32, 'dmap.contentcodesnumber'),
|
|
156
|
+
TagDefinition.new('miid', :uint32, 'dmap.itemid'),
|
|
157
|
+
TagDefinition.new('mcti', :uint32, 'dmap.containeritemid'),
|
|
158
|
+
TagDefinition.new('mpco', :uint32, 'dmap.parentcontainerid'),
|
|
159
|
+
TagDefinition.new('mimc', :uint32, 'dmap.itemcount'),
|
|
160
|
+
TagDefinition.new('mrco', :uint32, 'dmap.returnedcount'),
|
|
161
|
+
TagDefinition.new('mtco', :uint32, 'dmap.containercount'),
|
|
162
|
+
TagDefinition.new('mstm', :uint32, 'dmap.timeoutinterval'),
|
|
163
|
+
TagDefinition.new('msdc', :uint32, 'dmap.databasescount'),
|
|
164
|
+
TagDefinition.new('msma', :uint32, 'dmap.speakermachineaddress'),
|
|
165
|
+
TagDefinition.new('mlid', :uint32, 'dmap.sessionid'),
|
|
166
|
+
TagDefinition.new('assr', :uint32, 'daap.songsamplerate'),
|
|
167
|
+
TagDefinition.new('assz', :uint32, 'daap.songsize'),
|
|
168
|
+
TagDefinition.new('asst', :uint32, 'daap.songstarttime'),
|
|
169
|
+
TagDefinition.new('assp', :uint32, 'daap.songstoptime'),
|
|
170
|
+
TagDefinition.new('astm', :uint32, 'daap.songtime'),
|
|
171
|
+
TagDefinition.new('msto', :uint32, 'dmap.utcoffset'),
|
|
172
|
+
TagDefinition.new('cmsr', :uint32, 'dmcp.mediarevision'),
|
|
173
|
+
TagDefinition.new('caas', :uint32, 'dacp.albumshuffle'),
|
|
174
|
+
TagDefinition.new('caar', :uint32, 'dacp.albumrepeat'),
|
|
175
|
+
TagDefinition.new('cant', :uint32, 'dacp.remainingtime'),
|
|
176
|
+
TagDefinition.new('cmmk', :uint32, 'dmcp.mediakind'),
|
|
177
|
+
TagDefinition.new('cast', :uint32, 'dacp.tracklength'),
|
|
178
|
+
TagDefinition.new('asai', :uint32, 'daap.songalbumid'),
|
|
179
|
+
TagDefinition.new('aeNV', :uint32, 'com.apple.itunes.norm-volume'),
|
|
180
|
+
TagDefinition.new('cmvo', :uint32, 'dmcp.volume'),
|
|
181
|
+
TagDefinition.new('mcty', :uint16, 'dmap.contentcodestype'),
|
|
182
|
+
TagDefinition.new('asbt', :uint16, 'daap.songsbeatsperminute'),
|
|
183
|
+
TagDefinition.new('asbr', :uint16, 'daap.songbitrate'),
|
|
184
|
+
TagDefinition.new('asdc', :uint16, 'daap.songdisccount'),
|
|
185
|
+
TagDefinition.new('asdn', :uint16, 'daap.songdiscnumber'),
|
|
186
|
+
TagDefinition.new('astc', :uint16, 'daap.songtrackcount'),
|
|
187
|
+
TagDefinition.new('astn', :uint16, 'daap.songtracknumber'),
|
|
188
|
+
TagDefinition.new('asyr', :uint16, 'daap.songyear'),
|
|
189
|
+
TagDefinition.new('ated', :uint16, 'daap.supportsextradata'),
|
|
190
|
+
TagDefinition.new('asgr', :uint16, 'daap.supportsgroups'),
|
|
191
|
+
TagDefinition.new('mikd', :byte, 'dmap.itemkind'),
|
|
192
|
+
TagDefinition.new('casu', :byte, 'dacp.su'),
|
|
193
|
+
TagDefinition.new('msau', :byte, 'dmap.authenticationmethod'),
|
|
194
|
+
TagDefinition.new('mstu', :byte, 'dmap.updatetype'),
|
|
195
|
+
TagDefinition.new('asrv', :byte, 'daap.songrelativevolume'),
|
|
196
|
+
TagDefinition.new('asur', :byte, 'daap.songuserrating'),
|
|
197
|
+
TagDefinition.new('asdk', :byte, 'daap.songdatakind'),
|
|
198
|
+
TagDefinition.new('caps', :byte, 'dacp.playstatus'),
|
|
199
|
+
TagDefinition.new('cash', :byte, 'dacp.shufflestate'),
|
|
200
|
+
TagDefinition.new('carp', :byte, 'dacp.repeatstate'),
|
|
201
|
+
TagDefinition.new('muty', :byte, 'dmap.updatetype'),
|
|
202
|
+
TagDefinition.new("f\215ch", :byte, 'dmap.haschildcontainers'),
|
|
203
|
+
TagDefinition.new('msas', :byte, 'dmap.authenticationschemes'),
|
|
204
|
+
TagDefinition.new('cavs', :bool, 'dacp.visualizer'), # Source: https://code.google.com/p/tunesremote-plus/source/browse/trunk/src/org/tunesremote/daap/Status.java
|
|
205
|
+
TagDefinition.new('cafs', :bool, 'dacp.fullscreen'),
|
|
206
|
+
TagDefinition.new('ceGS', :bool, 'com.apple.itunes.genius-selectable'),
|
|
207
|
+
TagDefinition.new('mslr', :bool, 'dmap.loginrequired'),
|
|
208
|
+
TagDefinition.new('msal', :bool, 'dmap.supportsautologout'),
|
|
209
|
+
TagDefinition.new('msup', :bool, 'dmap.supportsupdate'),
|
|
210
|
+
TagDefinition.new('mspi', :bool, 'dmap.supportspersistenids'),
|
|
211
|
+
TagDefinition.new('msex', :bool, 'dmap.supportsextensions'),
|
|
212
|
+
TagDefinition.new('msbr', :bool, 'dmap.supportsbrowse'),
|
|
213
|
+
TagDefinition.new('msqy', :bool, 'dmap.supportsquery'),
|
|
214
|
+
TagDefinition.new('msix', :bool, 'dmap.supportsindex'),
|
|
215
|
+
TagDefinition.new('msrs', :bool, 'dmap.supportsresolve'),
|
|
216
|
+
TagDefinition.new('asco', :bool, 'daap.songcompliation'),
|
|
217
|
+
TagDefinition.new('asdb', :bool, 'daap.songdisabled'),
|
|
218
|
+
TagDefinition.new('abpl', :bool, 'daap.baseplaylist'),
|
|
219
|
+
TagDefinition.new('aeSP', :bool, 'com.apple.itunes.smart-playlist'),
|
|
220
|
+
TagDefinition.new('aePP', :bool, 'com.apple.itunes.is-podcast-playlist'),
|
|
221
|
+
TagDefinition.new('aePS', :bool, 'com.apple.itunes.special-playlist'),
|
|
222
|
+
TagDefinition.new('aeSG', :bool, 'com.apple.itunes.saved-genius'),
|
|
223
|
+
TagDefinition.new('aeFP', :bool, 'com.apple.itunes.req-fplay'),
|
|
224
|
+
TagDefinition.new('aeHV', :bool, 'com.apple.itunes.has-video'),
|
|
225
|
+
TagDefinition.new('caia', :bool, 'dacp.isavailiable'),
|
|
226
|
+
TagDefinition.new('ceVO', :bool, 'com.apple.itunes.voting-enabled'), # not an official name
|
|
227
|
+
TagDefinition.new('aeSV', :version, 'com.apple.itunes.music-sharing-version'),
|
|
228
|
+
TagDefinition.new('mpro', :version, 'dmap.protocolversion'),
|
|
229
|
+
TagDefinition.new('apro', :version, 'daap.protocolversion'),
|
|
230
|
+
TagDefinition.new('musr', :version, 'dmap.serverrevision'),
|
|
231
|
+
TagDefinition.new('mstc', :date, 'dmap.utc-time'),
|
|
232
|
+
TagDefinition.new('asda', :date, 'daap.songdateadded'),
|
|
233
|
+
TagDefinition.new('asdm', :date, 'daap.songdatemodified'),
|
|
234
|
+
TagDefinition.new('ceJC', :bool, 'com.apple.itunes.jukebox-client-vote'),
|
|
235
|
+
TagDefinition.new('ceJI', :bool, 'com.apple.itunes.jukebox-current'),
|
|
236
|
+
TagDefinition.new('ceJS', :uint16, 'com.apple.itunes.jukebox-score'),
|
|
237
|
+
TagDefinition.new('ceJV', :bool, 'com.apple.itunes.jukebox-vote'),
|
|
238
|
+
TagDefinition.new('ceQR', :container, 'com.apple.itunes.playqueue-contents-response'),
|
|
239
|
+
TagDefinition.new('ceQS', :container, 'com.apple.itunes.playqueue-contents-???'),
|
|
240
|
+
TagDefinition.new('ceQs', :uint64, 'com.apple.itunes.playqueue-id'), # non-official name
|
|
241
|
+
TagDefinition.new('ceQa', :string, 'com.apple.itunes.playqueue-album'),
|
|
242
|
+
TagDefinition.new('ceQg', :string, 'com.apple.itunes.playqueue-genre'),
|
|
243
|
+
TagDefinition.new('ceQn', :string, 'com.apple.itunes.playqueue-name'),
|
|
244
|
+
TagDefinition.new('ceQr', :string, 'com.apple.itunes.playqueue-artist'),
|
|
245
|
+
TagDefinition.new('msml', :container, 'msml'),
|
|
246
|
+
TagDefinition.new('aeGs', :bool, 'com.apple.itunes.can-be-genius-seed'),
|
|
247
|
+
TagDefinition.new('aprm', :short, 'daap.playlistrepeatmode'),
|
|
248
|
+
TagDefinition.new('apsm', :short, 'daap.playlistshufflemode'),
|
|
249
|
+
|
|
250
|
+
].freeze
|
|
251
|
+
end
|
|
252
|
+
end
|
data/lib/dacpclient.rb
ADDED
data/remoteclient.rb
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
$LOAD_PATH.unshift __dir__
|
|
2
|
+
require File.expand_path('lib/dacpclient.rb', __dir__)
|
|
3
|
+
require 'english'
|
|
4
|
+
require 'socket'
|
|
5
|
+
include DACPClient
|
|
6
|
+
class CLIClient
|
|
7
|
+
def initialize
|
|
8
|
+
@client = DACPClient::Client.new("CLIClient (#{Socket.gethostname})", 'localhost', 3689)
|
|
9
|
+
@login = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse_arguments(arguments)
|
|
13
|
+
if arguments.length > 0 && !([:parse_arguments].include?(arguments.first.to_sym)) && self.class.instance_methods(false).include?(arguments.first.to_sym)
|
|
14
|
+
method = arguments.first.to_sym
|
|
15
|
+
send method
|
|
16
|
+
status unless %i{help usage}.include?(method)
|
|
17
|
+
else
|
|
18
|
+
usage
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def status
|
|
23
|
+
login
|
|
24
|
+
status = @client.status
|
|
25
|
+
if status.caps == 2
|
|
26
|
+
puts '[STOPPED]'
|
|
27
|
+
else
|
|
28
|
+
name = status.cann
|
|
29
|
+
artist = status.cana
|
|
30
|
+
album = status.canl
|
|
31
|
+
playstatus = status.caps != 4 ? 'paused' : 'playing'
|
|
32
|
+
remaining = status.cant
|
|
33
|
+
total = status.cast
|
|
34
|
+
current = total - remaining
|
|
35
|
+
puts "[#{playstatus.upcase} (#{format_time(current)}/#{format_time(total)})] #{name} - #{artist} (#{album})"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def play
|
|
40
|
+
login
|
|
41
|
+
@client.play
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def pause
|
|
45
|
+
login
|
|
46
|
+
@client.pause
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def playpause
|
|
50
|
+
login
|
|
51
|
+
@client.playpause
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next
|
|
55
|
+
login
|
|
56
|
+
@client.next
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def prev
|
|
60
|
+
login
|
|
61
|
+
@client.prev
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def databases
|
|
65
|
+
login
|
|
66
|
+
puts @client.databases
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def playqueue
|
|
70
|
+
login
|
|
71
|
+
puts @client.list_queue
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def upnext
|
|
75
|
+
login
|
|
76
|
+
items = @client.list_queue.mlcl.select { |item| item.type.tag == 'mlit' }
|
|
77
|
+
puts 'Up next:'
|
|
78
|
+
puts '--------'
|
|
79
|
+
puts
|
|
80
|
+
items.each do |item|
|
|
81
|
+
name = item.ceQn
|
|
82
|
+
artist = item.ceQr
|
|
83
|
+
album = item.ceQa
|
|
84
|
+
puts "#{name} - #{artist} (#{album})"
|
|
85
|
+
end
|
|
86
|
+
puts
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def debug
|
|
90
|
+
login
|
|
91
|
+
require 'pry'
|
|
92
|
+
binding.pry
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def usage
|
|
96
|
+
puts "Usage: #{$PROGRAM_NAME} [command]"
|
|
97
|
+
puts
|
|
98
|
+
puts 'Where command is one of the following:'
|
|
99
|
+
|
|
100
|
+
puts CLIClient.instance_methods(false).reject { |m| [:parse_arguments].include?(m) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
alias_method :previous, :prev
|
|
104
|
+
alias_method :help, :usage
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def login
|
|
109
|
+
@client.login unless @login
|
|
110
|
+
@login = true
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def format_time(millis)
|
|
114
|
+
seconds, millis = millis.divmod(1000)
|
|
115
|
+
minutes, seconds = seconds.divmod(60)
|
|
116
|
+
hours, minutes = minutes.divmod(60)
|
|
117
|
+
if hours == 0
|
|
118
|
+
sprintf('%02d:%02d', minutes, seconds)
|
|
119
|
+
else
|
|
120
|
+
sprintf('%02d:%02d:%02d', hours, minutes, seconds)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
cli = CLIClient.new
|
|
126
|
+
cli.parse_arguments(Array(ARGV))
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dacpclient
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jurriaan Pruis
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2013-08-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dnssd
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ~>
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ~>
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: yard
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '>='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '>='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: redcarpet
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - '>='
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - '>='
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: github-markup
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - '>='
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - '>='
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
description: A DACP (iTunes Remote protocol) client written in the wonderful Ruby
|
|
70
|
+
language
|
|
71
|
+
email:
|
|
72
|
+
- email@jurriaanpruis.nl
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files:
|
|
76
|
+
- README.md
|
|
77
|
+
- LICENSE
|
|
78
|
+
files:
|
|
79
|
+
- .rubocop.yml
|
|
80
|
+
- Gemfile
|
|
81
|
+
- Gemfile.lock
|
|
82
|
+
- LICENSE
|
|
83
|
+
- README.md
|
|
84
|
+
- Rakefile
|
|
85
|
+
- TODO
|
|
86
|
+
- dacpclient.gemspec
|
|
87
|
+
- lib/dacpclient.rb
|
|
88
|
+
- lib/dacpclient/client.rb
|
|
89
|
+
- lib/dacpclient/dmapbuilder.rb
|
|
90
|
+
- lib/dacpclient/dmapconverter.rb
|
|
91
|
+
- lib/dacpclient/dmapparser.rb
|
|
92
|
+
- lib/dacpclient/pairingserver.rb
|
|
93
|
+
- lib/dacpclient/tagdefinitions.rb
|
|
94
|
+
- lib/dacpclient/version.rb
|
|
95
|
+
- remoteclient.rb
|
|
96
|
+
homepage: https://github.com/jurriaan/ruby-dacpclient
|
|
97
|
+
licenses:
|
|
98
|
+
- MIT
|
|
99
|
+
metadata: {}
|
|
100
|
+
post_install_message:
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - '>='
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: 2.0.0
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - '>='
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubyforge_project:
|
|
116
|
+
rubygems_version: 2.0.5
|
|
117
|
+
signing_key:
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: A DACP (iTunes Remote protocol) client written in the wonderful Ruby language
|
|
120
|
+
test_files: []
|
|
121
|
+
has_rdoc:
|