dacpclient 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml 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 :)