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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 851490abf8148566e8348090625db3b72d2d6328
4
- data.tar.gz: c845ff13c853f52927eea02ead5465469a461f1e
3
+ metadata.gz: 1a91f6ef4321ce83e0cabc85493552ebf7961756
4
+ data.tar.gz: c032e8102b42801e7fa220861ecefb5b306f0550
5
5
  SHA512:
6
- metadata.gz: 528948172ba3b6bd76e0cb3cc4ccf15508df58a86267035e65a07b2e489fa53cbcfe57cc868f4641bb18d3921878a04edb61b523f1fc9b01761cca563cc0320b
7
- data.tar.gz: 1530e1c99b1d139467fd9638c617fa1fd95f4793abff03a89cad81a07052fa014a7ced470df0a12328663e3d5fd2d96aacbeaedf71123a75fabaa7a1d41a71f9
6
+ metadata.gz: 352cf3561c859d1bf52deb7a3fb33d036245282e6610e72871f11863055afebd3fb41ff630fdb887c3f52573bf3ead3fbcf9fc7163ca0b6882391d9addf65ec6
7
+ data.tar.gz: e78e5bad668a556c7d583702c23798a75b2df288bfb8358c9e750047685963bfbbc3380f9c2df6c12fb9236a3b5c2bb65f9b8bd5ebc6950a9c3801754cac72f6
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ *.rbc
4
+ .bundle
5
+ .config
6
+ coverage
7
+ InstalledFiles
8
+ lib/bundler/man
9
+ pkg
10
+ rdoc
11
+ spec/reports
12
+ test/tmp
13
+ test/version_tmp
14
+ tmp
15
+ .DS_Store
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  # Avoid methods longer than 10 lines of code
2
2
  MethodLength:
3
3
  Enabled: false
4
- LineLength:
4
+ Documentation:
5
+ Enabled: false
6
+ ConstantName:
5
7
  Enabled: false
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', github: 'bbatsov/rubocop', ref: '68b73d5c38a' #'~> 0.12.0'
12
- gem 'simplecov', require: false
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
- A DACP (iTunes Remote protocol) client written in the wonderful Ruby language
1
+ #DACPClient
2
2
 
3
- Look at the remoteclient.rb file for an example client.
3
+ [![Gem Version](https://badge.fury.io/rb/dacpclient.png)](http://badge.fury.io/rb/dacpclient) [![Dependency Status](https://gemnasium.com/jurriaan/ruby-dacpclient.png)](https://gemnasium.com/jurriaan/ruby-dacpclient)
4
+
5
+ A DACP (iTunes Remote protocol) client written in the wonderful Ruby language.
6
+ You can use this for controlling iTunes. It uses the same protocol as the iTunes remote iOS app.
7
+
8
+ Look at the [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient) file for an example client.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'dacpclient'
15
+
16
+ And then execute:
17
+
18
+ bundle
19
+
20
+ Or install it yourself using:
21
+
22
+ gem install dacpclient
23
+
24
+ ## Usage
25
+
26
+ See [bin/dacpclient](https://github.com/jurriaan/ruby-dacpclient/blob/master/bin/dacpclient)
27
+
28
+ ## Todo
29
+
30
+ - Add tests
31
+ - Add more tagdefinitions
32
+ - Documentation
33
+
34
+ ## Contributing
35
+
36
+ 1. Fork it
37
+ 2. Create your feature branch (`git checkout -tb my-new-feature`)
38
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
39
+ 4. Push to the branch (`git push origin my-new-feature`)
40
+ 5. Create new Pull Request
41
+
42
+ ## Contributors
43
+
44
+ - [Jurriaan Pruis](https://github.com/jurriaan)
45
+
46
+ ## Thanks
47
+
48
+ - [edc1591](https://github.com/edc1591) - for some of the 'Up Next' code
@@ -1,8 +1,10 @@
1
- $LOAD_PATH.unshift __dir__
2
- require File.expand_path('lib/dacpclient.rb', __dir__)
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'dacpclient'
3
4
  require 'english'
4
5
  require 'socket'
5
- include DACPClient
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 %i{help usage}.include?(method)
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 ? 'paused' : 'playing'
33
+ playstatus = status.caps != 4 ? '❙❙' : ''
32
34
  remaining = status.cant
33
35
  total = status.cast
34
36
  current = total - remaining
35
- puts "[#{playstatus.upcase} (#{format_time(current)}/#{format_time(total)})] #{name} - #{artist} (#{album})"
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 written in the wonderful Ruby language'
11
- spec.summary = 'A DACP (iTunes Remote protocol) client written in the wonderful Ruby language'
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
- spec.required_ruby_version = '>= 2.0.0'
27
+
28
+ spec.required_ruby_version = '>= 1.9.3'
28
29
  end
@@ -1,15 +1,16 @@
1
- $LOAD_PATH.unshift __dir__
2
1
  require 'rubygems'
3
- require 'bundler/setup'
2
+ require 'bundler'
3
+ Bundler.setup(:default)
4
4
  require 'digest'
5
5
  require 'net/http'
6
- require_relative 'pairingserver'
7
- require_relative 'dmapparser'
8
- require_relative 'dmapbuilder'
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 = nil)
26
+ def pair(pin)
26
27
  pairingserver = PairingServer.new(@name, '0.0.0.0', 1024)
27
- pairingserver.pin = pin unless pin.nil?
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
- response = do_action(:login, { 'pairing-guid' => '0x' + Client.get_guid(@name) })
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 'playqueue-edit', { command: 'add', query: "\'dmap.itemid:#{id}\'" }
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 'playqueue-edit', { command: 'clear' }
140
+ do_action('playqueue-edit', { command: 'clear' })
131
141
  end
132
142
 
133
143
  def list_queue
134
- do_action 'playqueue-contents', {}
144
+ do_action('playqueue-contents', {})
135
145
  end
136
146
 
137
147
  def databases
138
- do_action 'databases', {}, true
148
+ do_action('databases', {}, true)
139
149
  end
140
150
 
141
151
  def playlists(db)
142
- do_action "databases/#{db}/containers", {}, true
152
+ do_action("databases/#{db}/containers", {}, true)
143
153
  end
144
154
 
145
- def artwork(database, id, width = 320, height = 320)
146
- do_action "databases/#{database}/items/#{id}/extra_data/artwork", { mw: width, mh: height }, true
155
+ def default_db
156
+ databases[:mlcl].to_a.find {|item| item.mdbk == 1}
147
157
  end
148
158
 
149
- def now_playing_artwork(width = 320, height = 320)
150
- do_action :nowplayingartwork, { mw: width, mh: height }
159
+ def default_playlist(db)
160
+ @client.playlists(72).mlcl.to_a.find { |item| item.abpl }
151
161
  end
152
162
 
153
- def search(db, container, search)
154
- words = search.split
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
- queries.push(words.map { |v| "\'dmap.itemname:*#{v}*\'" }.join('+'))
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
- 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
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, 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 })
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 { |http| http.request(req) }
175
- if res.kind_of?(Net::HTTPServiceUnavailable) || res.kind_of?(Net::HTTPForbidden)
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
- p res
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
- self.new.send(method, *args, &block)
11
+ new.send(method, *args, &block)
11
12
  end
12
13
 
13
14
  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 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
- @dmap_stack.last.value << DMAPParser::Tag.new(tag, args.size > 1 ? args : args.first)
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,4 +1,5 @@
1
1
  module DACPClient
2
+ # The DMAPConverter class converts between binary and ruby formats
2
3
  class DMAPConverter
3
4
  class << self
4
5
  def bin_to_byte(data)
@@ -1,82 +1,86 @@
1
- require_relative 'tagdefinitions'
2
- require_relative 'dmapconverter'
1
+ require 'dacpclient/tagdefinitions'
2
+ require 'dacpclient/dmapconverter'
3
3
 
4
4
  require 'stringio'
5
5
  module DACPClient
6
- class DMAPParser
6
+ module DMAPParser
7
+ # The DMAPParser class parses DMAP responses
8
+ class Parser
7
9
 
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
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
- private
21
+ private
20
22
 
21
- def self.parse_container(response)
22
- values = []
23
+ def self.parse_container(response)
24
+ values = []
23
25
 
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))
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
- 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
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
- 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
- })
18
+ # puts "Pairing started (pincode=#{@pin.join})"
29
19
 
30
- pairingstring = DMAPBuilder.cmpa do
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
- @service = DNSSD.register!(@name, '_touch-remote._tcp', 'local', @port, txtrecord)
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\nContent-Length: #{pairingstring.length}\r\n\r\n#{pairingstring}"
46
- puts 'Pairing succeeded :)'
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
- class DMAPParser
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
- puts "Unknown type #{tag.type}"
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('') { |a, e| a + e.inspect(level + 1).chomp + "\n" }
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
- else
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
- value.each do |dmap|
75
- return dmap.value if dmap.type.tag.to_s == method.to_s
76
- end
77
- super
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'), # not a official name?
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'), # Source: https://code.google.com/p/tunesremote-plus/source/browse/trunk/src/org/tunesremote/daap/Status.java
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'), # not an official name
227
- TagDefinition.new('aeSV', :version, 'com.apple.itunes.music-sharing-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, '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
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
@@ -1,3 +1,4 @@
1
+ # The DACPClient module
1
2
  module DACPClient
2
- VERSION = '0.1.0'
3
+ VERSION = '0.1.1'
3
4
  end
data/lib/dacpclient.rb CHANGED
@@ -1,2 +1 @@
1
- $LOAD_PATH.unshift __dir__
2
1
  require 'dacpclient/client'
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.0
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-12 00:00:00.000000000 Z
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 written in the wonderful Ruby
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
- - TODO
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: 2.0.0
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 written in the wonderful Ruby language
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
data/TODO DELETED
@@ -1,3 +0,0 @@
1
- - Add tests (DMAPBuilder)
2
- - Add more tagdefinitions
3
- - Documentation :)