dacpclient 0.2.6 → 0.2.9

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: 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