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 +4 -4
- data/.rubocop.yml +4 -1
- data/Gemfile +1 -1
- data/Rakefile +8 -6
- data/bin/dacpclient +131 -101
- data/dacpclient.gemspec +3 -1
- data/lib/dacpclient/browser.rb +64 -0
- data/lib/dacpclient/client.rb +77 -49
- data/lib/dacpclient/faraday/flatter_params_encoder.rb +77 -0
- data/lib/dacpclient/model.rb +117 -0
- data/lib/dacpclient/models/pair_info.rb +15 -0
- data/lib/dacpclient/models/play_queue.rb +11 -0
- data/lib/dacpclient/models/play_queue_item.rb +17 -0
- data/lib/dacpclient/models/playlist.rb +9 -0
- data/lib/dacpclient/models/playlists.rb +10 -0
- data/lib/dacpclient/models/status.rb +44 -0
- data/lib/dacpclient/pairingserver.rb +21 -13
- data/lib/dacpclient/version.rb +1 -1
- metadata +42 -12
- data/lib/dacpclient/dmapbuilder.rb +0 -43
- data/lib/dacpclient/dmapconverter.rb +0 -119
- data/lib/dacpclient/dmapparser.rb +0 -40
- data/lib/dacpclient/tag.rb +0 -21
- data/lib/dacpclient/tag_container.rb +0 -51
- data/lib/dacpclient/tag_definition.rb +0 -29
- data/lib/dacpclient/tag_definitions.rb +0 -167
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14943f6c9ee6b10cc14575ab008cea5ba669bef0
|
4
|
+
data.tar.gz: 0d2df609607cf2f3f4f4fe949e033ea137b809fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 282d58e2556fa3f6c605ef6fe73348f23795c3113d509066f995e1efdb2c8ded311839bc7801881852b5dc3c0d7e4cd684b4557b596600aaf94bc5803cdd70ef
|
7
|
+
data.tar.gz: e5d45be377ad35efdd2a4551cec7bf5fe582bd435f6d383a02dee5bd70e9869f9fee4f78ea84a1970dce1820626b271113117ec018bdb4750452bdfc6ecce617
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
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['
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
54
|
+
desc :status, 'Shows the status of the DACP server'
|
55
|
+
method_options ticker: :boolean
|
56
|
+
def status
|
58
57
|
login
|
59
|
-
|
60
|
-
|
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
|
64
|
+
puts 'Setting up Home Sharing. Saving Home Sharing GUID to ' + config_file
|
89
65
|
puts "\nPlease enter your Apple ID credentials:"
|
90
|
-
|
91
|
-
|
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
|
-
|
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'],
|
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
|
-
|
138
|
-
|
139
|
-
puts @client.databases
|
140
|
-
end
|
141
|
-
|
142
|
-
def playqueue
|
117
|
+
desc :playlists, 'Show the playlists'
|
118
|
+
def playlists
|
143
119
|
login
|
144
|
-
|
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.
|
133
|
+
items = @client.list_queue.items
|
150
134
|
puts 'Up next:'
|
151
135
|
puts '--------'
|
152
136
|
puts
|
153
137
|
items.each do |item|
|
154
|
-
|
155
|
-
artist = item.
|
156
|
-
album = item.
|
157
|
-
puts "#{
|
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
|
-
|
170
|
-
|
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
|
-
|
174
|
-
|
175
|
-
puts "
|
176
|
-
puts
|
177
|
-
|
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,
|
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
|
-
|
227
|
-
|
228
|
-
@config.merge!
|
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
|
-
|
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.
|
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
|