dacpclient 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,5 @@
1
+ # Avoid methods longer than 10 lines of code
2
+ MethodLength:
3
+ Enabled: false
4
+ LineLength:
5
+ Enabled: false
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
@@ -0,0 +1,3 @@
1
+ A DACP (iTunes Remote protocol) client written in the wonderful Ruby language
2
+
3
+ Look at the remoteclient.rb file for an example client.
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
@@ -0,0 +1,3 @@
1
+ - Add tests (DMAPBuilder)
2
+ - Add more tagdefinitions
3
+ - Documentation :)
@@ -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
@@ -0,0 +1,3 @@
1
+ module DACPClient
2
+ VERSION = '0.1.0'
3
+ end
data/lib/dacpclient.rb ADDED
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift __dir__
2
+ require 'dacpclient/client'
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: