dacpclient 0.2.6 → 0.2.9

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: 26af62b1cfad48c22cd9703ca63ccef94c68869c
4
- data.tar.gz: f11ddd282d4ec95b1811c153357227f83ecea644
3
+ metadata.gz: 14943f6c9ee6b10cc14575ab008cea5ba669bef0
4
+ data.tar.gz: 0d2df609607cf2f3f4f4fe949e033ea137b809fe
5
5
  SHA512:
6
- metadata.gz: 81e9606de9d867a38ad40ed0e0e85cdae6c6bea41e4abe2946f316984739cf118612ee24f295bb2820620da5c4f393711c3211cdb1e7b8b1551dd3935ef63c95
7
- data.tar.gz: e9f211355e6efbac62c8a8d96ad4f7489742e0caad33a46750ecd1c5137a8207d9a924e331e3b6aada91d8514837e61f8da60a8780f6b1c87c298a018d378297
6
+ metadata.gz: 282d58e2556fa3f6c605ef6fe73348f23795c3113d509066f995e1efdb2c8ded311839bc7801881852b5dc3c0d7e4cd684b4557b596600aaf94bc5803cdd70ef
7
+ data.tar.gz: e5d45be377ad35efdd2a4551cec7bf5fe582bd435f6d383a02dee5bd70e9869f9fee4f78ea84a1970dce1820626b271113117ec018bdb4750452bdfc6ecce617
data/.rubocop.yml CHANGED
@@ -6,4 +6,7 @@ Documentation:
6
6
  ClassVars:
7
7
  Enabled: false
8
8
  ClassLength:
9
- Enabled: false
9
+ Enabled: false
10
+ AllCops:
11
+ Excludes:
12
+ - flatter_params_encoder.rb
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
1
  gemspec
2
2
 
3
- source 'https://rubygems.org'
3
+ source 'https://rubygems.org'
data/Rakefile CHANGED
@@ -2,6 +2,7 @@ require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
  require 'rubocop'
4
4
  require 'yard'
5
+ require 'rubocop/rake_task'
5
6
  YARD::Rake::YardocTask.new
6
7
 
7
8
  # Rake::TestTask.new do |t|
@@ -10,13 +11,14 @@ YARD::Rake::YardocTask.new
10
11
  # t.verbose = true
11
12
  # end
12
13
 
13
- task test: :rubocop
14
+ task test: :rubocop do
15
+ end
14
16
 
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
17
+ Rubocop::RakeTask.new(:rubocop) do |task|
18
+ task.patterns = ['**/*.rb', 'Rakefile', 'dacpclient.gemspec', 'bin/*']
19
+ # don't abort rake on failure
20
+ task.options = ['-c', '.rubocop.yml']
21
+ task.fail_on_error = true
20
22
  end
21
23
 
22
24
  task default: :test
data/bin/dacpclient CHANGED
@@ -1,98 +1,71 @@
1
1
  #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift(File.join(__dir__, '../lib/'))
2
3
  require 'dacpclient'
3
4
  require 'English'
4
5
  require 'socket'
5
6
  require 'yaml'
6
7
  require 'fileutils'
7
8
  require 'io/console'
9
+ require 'thor'
8
10
 
9
11
  # This is the CLI DACP Client. Normally installed as `dacpclient`
10
- class CLIClient
11
- def initialize
12
+ class CLIClient < Thor
13
+ package_name :dacpclient
14
+ include Thor::Actions
15
+ def initialize(*)
12
16
  @config = {}
13
17
  @config['client_name'] ||= "DACPClient (#{Socket.gethostname})"
14
18
  @config['host'] ||= 'localhost'
19
+ @config['known_databases'] ||= ['']
20
+
15
21
  load_config
16
22
 
17
- @client = DACPClient::Client.new(@config['client_name'], @config['host'], 3689)
18
23
  if @config['guid'].nil? || @config['guid'] !~ /^[A-F0-9]{16}$/
19
- @config['guid'] = @client.guid
24
+ guid = Digest::SHA2.hexdigest(@config['client_name'])[0..15].upcase
25
+ @config['guid'] = guid
20
26
  save_config
21
27
  end
22
- @client.guid = @config['guid']
23
- @login = false
24
- end
25
28
 
26
- def parse_arguments(arguments)
27
- if arguments.length > 0 && !([:parse_arguments].include?(arguments.first.to_sym)) && self.class.instance_methods(false).include?(arguments.first.to_sym)
28
- method = arguments.first.to_sym
29
- send method
30
- status unless [:help, :usage, :status, :status_ticker].include?(method)
31
- else
32
- usage
29
+ browser = DACPClient::Browser.new
30
+ browser.browse(false)
31
+ database = browser.devices.find do |device|
32
+ @config['known_databases'].include? device.database_id
33
33
  end
34
- end
35
34
 
36
- def status
37
- login
38
- status = @client.status
39
- if status.caps == 2
40
- puts '[STOPPED]'
41
- else
42
- name = status.cann
43
- artist = status.cana
44
- album = status.canl
45
- playstatus = status.caps != 4 ? '❙❙' : '▶ '
46
- current = 0
47
- total = 0
48
- if status.cast? && status.cant?
49
- remaining = status.cant
50
- total = status.cast
51
- current = total - remaining
52
- end
53
- puts "[#{format_time(current)}/#{format_time(total)}] #{playstatus} #{name} - #{artist} (#{album})"
35
+ unless database
36
+ pin = 4.times.map { Random.rand(10) } if pin.nil?
37
+ puts 'Cannot find paired Libraries, waiting for a pair request..'
38
+ puts "Pincode: #{pin}"
39
+ pairserver = DACPClient::PairingServer.new(@config['client_name'],
40
+ @config['guid'])
41
+ pairserver.pin = pin
42
+ database = pairserver.start
43
+ @config['known_databases'] << database.database_id
44
+ save_config
54
45
  end
46
+ @client = DACPClient::Client.new(@config['client_name'], database.host,
47
+ database.port)
48
+ @client.guid = @config['guid']
49
+ @login = false
50
+
51
+ super
55
52
  end
56
53
 
57
- def status_ticker
54
+ desc :status, 'Shows the status of the DACP server'
55
+ method_options ticker: :boolean
56
+ def status
58
57
  login
59
- status = nil
60
- status_time = 0
61
- repeat_every(1) do
62
- unless status.nil?
63
- if status.caps == 2
64
- print "\r\033[K[STOPPED]"
65
- else
66
- name = status.cann
67
- artist = status.cana
68
- album = status.canl
69
- playstatus = status.caps != 4 ? '❙❙' : '▶ '
70
- current = 0
71
- total = 0
72
- if status.cast? && status.cant?
73
- remaining = status.cant
74
- total = status.cast
75
- current = total - remaining + [((Time.now.to_f * 1000.0) - status_time), 0].max
76
- end
77
- print "\r\033[K[#{format_time(current)}/#{format_time(total)}] #{playstatus} #{name} - #{artist} (#{album})"
78
- end
79
- end
80
- end
81
- loop do
82
- status = @client.status true
83
- status_time = Time.now.to_f * 1000.0
84
- end
58
+ return status_ticker if options[:ticker]
59
+ show_status
85
60
  end
86
61
 
62
+ desc :home_sharing, 'Setup Home Sharing (Not fully functional)'
87
63
  def home_sharing
88
- puts "Setting up Home Sharing. Saving Home Sharing GUID to ~/.dacpclient/config.yml"
64
+ puts 'Setting up Home Sharing. Saving Home Sharing GUID to ' + config_file
89
65
  puts "\nPlease enter your Apple ID credentials:"
90
- print "Apple ID (e-mail address): "
91
- email = $stdin.gets.strip
92
- print "Password: "
93
- password = $stdin.noecho(&:gets).chomp
66
+ email = ask('Apple ID (e-mail address):').strip
67
+ password = ask('Password:')
94
68
  guid = @client.setup_home_sharing(email, password)
95
- password = nil
96
69
  @config['appleid'] = email
97
70
  @config['hsgid'] = guid
98
71
  save_config
@@ -100,90 +73,140 @@ class CLIClient
100
73
  puts "Got your Home Sharing GUID (#{guid}). Logging in.."
101
74
  login
102
75
  end
103
-
76
+
77
+ desc :hostname, 'Set the hostname'
104
78
  def hostname
105
- puts "Please enter a new hostname to connect to:"
106
- @config['host'] = $stdin.gets.strip
79
+ @config['host'] = ask('Please enter a new hostname to connect to:').strip
107
80
  save_config
108
- @client = DACPClient::Client.new(@config['client_name'], @config['host'], 3689)
81
+ @client = DACPClient::Client.new(@config['client_name'], @config['host'],
82
+ 3689)
109
83
  status
110
84
  end
111
85
 
86
+ desc :play, 'Start playing'
112
87
  def play
113
88
  login
114
89
  @client.play
115
90
  end
116
91
 
92
+ desc :pause, 'Pause Playing'
117
93
  def pause
118
94
  login
119
95
  @client.pause
120
96
  end
121
97
 
98
+ desc :playpause, 'Toggle Playing'
122
99
  def playpause
123
100
  login
124
101
  @client.playpause
125
102
  end
126
103
 
104
+ desc :next, 'Go to next item'
127
105
  def next
128
106
  login
129
107
  @client.next
130
108
  end
131
109
 
110
+ desc :prev, 'Go to previous item'
111
+ map previous: :prev
132
112
  def prev
133
113
  login
134
114
  @client.prev
135
115
  end
136
116
 
137
- def databases
138
- login
139
- puts @client.databases
140
- end
141
-
142
- def playqueue
117
+ desc :playlists, 'Show the playlists'
118
+ def playlists
143
119
  login
144
- puts @client.list_queue
120
+ playlist_items = @client.playlists
121
+ puts 'Playlists:'
122
+ puts '----------'
123
+ playlist_items.each do |playlist|
124
+ print ' ' unless playlist.base_playlist?
125
+ puts "#{playlist.name} (#{playlist.count})"
126
+ end
127
+ puts
145
128
  end
146
129
 
130
+ desc :upnext, 'Show what\'s up next'
147
131
  def upnext
148
132
  login
149
- items = @client.list_queue.mlcl.to_a.select {|i| i.type.tag == 'mlit' }
133
+ items = @client.list_queue.items
150
134
  puts 'Up next:'
151
135
  puts '--------'
152
136
  puts
153
137
  items.each do |item|
154
- name = item.ceQn
155
- artist = item.ceQr
156
- album = item.ceQa
157
- puts "#{name} - #{artist} (#{album})"
138
+ title = item.title
139
+ artist = item.artist
140
+ album = item.album
141
+ puts "#{title} - #{artist} (#{album}) [#{format_time(item.song_time)}]"
158
142
  end
159
143
  puts
160
144
  end
161
145
 
146
+ desc :stop, 'Stop playing'
162
147
  def stop
163
148
  login
164
149
  @client.stop
165
150
  end
166
151
 
152
+ # rubocop:disable Debugger
153
+ desc :debug, 'Debuggin\''
167
154
  def debug
168
155
  login
169
- require 'pry'
170
- binding.pry
156
+ begin
157
+ require 'pry'
158
+ binding.pry
159
+ rescue
160
+ puts 'Please install PRY to be able to debug things.'
161
+ end
171
162
  end
163
+ # rubocop:enable Debugger
172
164
 
173
- def usage
174
- puts "Usage: #{$PROGRAM_NAME} [command]"
175
- puts "(c) #{Time.now.year} Jurriaan Pruis <email@jurriaanpruis.nl>"
176
- puts
177
- puts 'Where command is one of the following:'
178
-
179
- puts CLIClient.instance_methods(false).reject { |m| [:parse_arguments].include?(m) }
165
+ desc :version, 'Show DACPClient Version'
166
+ def version
167
+ puts "DACPClient v#{DACPClient::VERSION}"
168
+ puts "using DMAPParser v#{DMAPParser::VERSION}"
169
+ print "DACPClient and DMAPParser are Copyright (c) "
170
+ puts "#{Time.now.year} Jurriaan Pruis"
180
171
  end
181
172
 
182
- alias_method :previous, :prev
183
- alias_method :help, :usage
184
-
185
173
  private
186
174
 
175
+ def show_status(status = @client.status, start_time = nil)
176
+ name = status.title
177
+ artist = status.artist
178
+ album = status.album
179
+ playstatus = status.playing? ? '▶ ' : '❙❙'
180
+ current = 0
181
+ total = 0
182
+ extra_time = 0
183
+ extra_time = Time.now.to_f * 1000.0 - start_time if start_time
184
+ if status.song_length? && status.song_remaining_time?
185
+ total = status.song_length
186
+ current = status.song_position + extra_time
187
+ end
188
+ print "[#{format_time(current)}/#{format_time(total)}]"
189
+ puts " #{playstatus} #{name} - #{artist} (#{album})"
190
+ end
191
+
192
+ def status_ticker
193
+ status = nil
194
+ start_time = nil
195
+ repeat_every(1) do
196
+ unless status.nil?
197
+ if status.stopped?
198
+ print "\r\033[K[STOPPED]"
199
+ else
200
+ show_status(status, start_time)
201
+ end
202
+ end
203
+ end
204
+ loop do
205
+ status = @client.status true
206
+ start_time = Time.now.to_f * 1000.0
207
+ end
208
+ end
209
+
187
210
  def login
188
211
  return if @login
189
212
  @client.hsgid = @config['hsgid']
@@ -193,10 +216,14 @@ class CLIClient
193
216
  @client.login
194
217
  end
195
218
  @login = true
219
+ if @client.host != @config['host']
220
+ @config['host'] = @client.host
221
+ save_config
222
+ end
196
223
  end
197
224
 
198
225
  def format_time(millis)
199
- seconds, millis = millis.divmod(1000)
226
+ seconds, _ = millis.divmod(1000)
200
227
  minutes, seconds = seconds.divmod(60)
201
228
  hours, minutes = minutes.divmod(60)
202
229
  if hours == 0
@@ -221,22 +248,25 @@ class CLIClient
221
248
  File.join(ENV['HOME'], '.dacpclient')
222
249
  end
223
250
 
251
+ def config_file
252
+ File.join(config_dir, 'config.yml')
253
+ end
254
+
224
255
  def load_config
225
256
  FileUtils.mkdir_p(config_dir)
226
- config_file = File.join(config_dir,'config.yml')
227
- if File.exists? config_file
228
- @config.merge! YAML.load_file(config_file)
257
+ if File.exist? config_file
258
+ data = YAML.load_file(config_file)
259
+ @config.merge!(data) if data.is_a?(Hash)
229
260
  else
230
261
  save_config
231
262
  end
232
263
  end
233
264
 
234
265
  def save_config
235
- File.open(File.join(config_dir,'config.yml'), 'w') do |out|
266
+ File.open(File.join(config_dir, 'config.yml'), 'w') do |out|
236
267
  YAML.dump(@config, out)
237
268
  end
238
269
  end
239
270
  end
240
271
 
241
- cli = CLIClient.new
242
- cli.parse_arguments(Array(ARGV))
272
+ CLIClient.start
data/dacpclient.gemspec CHANGED
@@ -22,12 +22,14 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency 'faraday', '~> 0.9.0'
23
23
  spec.add_runtime_dependency 'dnssd', '~> 2.0'
24
24
  spec.add_runtime_dependency 'plist', '~> 3.1.0'
25
+ spec.add_runtime_dependency 'dmapparser', '~> 0.0.2'
26
+ spec.add_runtime_dependency 'thor', '~> 0.18.1'
25
27
 
26
28
  spec.add_development_dependency 'yard'
27
29
  spec.add_development_dependency 'redcarpet'
28
30
  spec.add_development_dependency 'github-markup'
29
31
  spec.add_development_dependency 'minitest', '~> 5.2.0'
30
- spec.add_development_dependency 'rubocop', '~> 0.17.0'
32
+ spec.add_development_dependency 'rubocop', '~> 0.18.0'
31
33
  spec.add_development_dependency 'rake'
32
34
 
33
35
  spec.required_ruby_version = '>= 2.0.0'
@@ -0,0 +1,64 @@
1
+ require 'timeout'
2
+ require 'ostruct'
3
+
4
+ module DACPClient
5
+ # The Client class handles communication with the server
6
+ class Browser
7
+ class Device < Struct.new(:host, :port, :text_records)
8
+ def name
9
+ text_records['Machine Name'] || text_records['CtlN']
10
+ end
11
+
12
+ def database_id
13
+ text_records['Database ID'] || text_records['DbId']
14
+ end
15
+ end
16
+
17
+ DAAP_SERVICE = '_daap._tcp'.freeze
18
+ TOUCHABLE_SERVICE = '_touch-able._tcp'.freeze
19
+
20
+ attr_reader :devices
21
+
22
+ def initialize
23
+ @devices = []
24
+ end
25
+
26
+ def browse(new_service = true)
27
+ service_name = new_service ? DAAP_SERVICE : TOUCHABLE_SERVICE
28
+ @devices = []
29
+ timeout(2) do
30
+ DNSSD.browse!(service_name) do |node|
31
+ resolve(node)
32
+ break unless node.flags.more_coming?
33
+ end
34
+ end
35
+ devices
36
+ rescue Timeout::Error # => e
37
+ []
38
+ end
39
+
40
+ private
41
+
42
+ def node_resolver(node, resolved)
43
+ devices << Device.new(get_device_host(resolved), resolved.port,
44
+ resolved.text_record)
45
+
46
+ resolved.flags.more_coming?
47
+ end
48
+
49
+ def get_device_host(resolved)
50
+ target = resolved.target
51
+ info = Socket.getaddrinfo(target, nil, Socket::AF_INET)
52
+ info[0][2]
53
+ rescue SocketError
54
+ target
55
+ end
56
+
57
+ def resolve(node)
58
+ resolver = DNSSD::Service.new
59
+ resolver.resolve(node) do |resolved|
60
+ break unless node_resolver(node, resolved)
61
+ end
62
+ end
63
+ end
64
+ end