rmpd 1.0.5 → 1.1.0
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.
- data/console.rb +15 -0
- data/lib/rmpd.rb +3 -1
- data/lib/rmpd/command.rb +155 -0
- data/lib/rmpd/commands.rb +40 -10
- data/lib/rmpd/commands/admin.rb +4 -4
- data/lib/rmpd/commands/database.rb +7 -7
- data/lib/rmpd/commands/generators.rb +6 -32
- data/lib/rmpd/commands/miscellaneous.rb +7 -50
- data/lib/rmpd/commands/playlist.rb +9 -24
- data/lib/rmpd/config.rb +18 -1
- data/lib/rmpd/connection.rb +38 -41
- data/lib/rmpd/response.rb +134 -23
- data/lib/rmpd/response_splitter.rb +26 -0
- data/lib/rmpd/version.rb +1 -1
- data/rmpd.gemspec +1 -0
- data/spec/models/commands_spec.rb +0 -10
- data/spec/models/config_spec.rb +4 -0
- data/spec/models/connection_spec.rb +2 -0
- data/spec/models/generators_spec.rb +2 -9
- data/spec/models/response_spec.rb +4 -7
- metadata +21 -6
- data/lib/rmpd/multi_response.rb +0 -43
- data/spec/models/multi_response_spec.rb +0 -54
data/console.rb
ADDED
data/lib/rmpd.rb
CHANGED
@@ -1,13 +1,15 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), "rmpd/config")
|
2
|
+
require File.join(File.dirname(__FILE__), "rmpd/command")
|
2
3
|
require File.join(File.dirname(__FILE__), "rmpd/commands")
|
3
4
|
require File.join(File.dirname(__FILE__), "rmpd/connection")
|
4
|
-
require File.join(File.dirname(__FILE__), "rmpd/multi_response")
|
5
5
|
require File.join(File.dirname(__FILE__), "rmpd/response")
|
6
6
|
|
7
7
|
module Rmpd
|
8
8
|
ACK_RE = /^ACK \[(\d+)@(\d+)\] \{([^}]*)\} (.*)$/
|
9
9
|
OK_RE = /^OK.*$/
|
10
10
|
LIST_OK_RE = /^list_OK.*$/
|
11
|
+
PROTOCOL_RE = /^OK MPD (\d+)\.(\d+)\.(\d+)$/
|
12
|
+
END_RE = Regexp.union(ACK_RE, OK_RE, PROTOCOL_RE)
|
11
13
|
|
12
14
|
class MpdError < StandardError ; end
|
13
15
|
|
data/lib/rmpd/command.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require "delegate"
|
2
|
+
require "forwardable"
|
3
|
+
|
4
|
+
|
5
|
+
module Rmpd
|
6
|
+
class Command
|
7
|
+
|
8
|
+
def self.new(name)
|
9
|
+
obj = super
|
10
|
+
obj.extend(choose_strategy(name))
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(name)
|
14
|
+
@name = name
|
15
|
+
@list = initialize_list(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def self.choose_strategy(name)
|
22
|
+
if /command_list_ok/ === name
|
23
|
+
CommandListOkStrategy
|
24
|
+
elsif /command_list/ === name
|
25
|
+
CommandListStrategy
|
26
|
+
else
|
27
|
+
CommandStrategy
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize_list
|
32
|
+
list = List.new
|
33
|
+
yield list
|
34
|
+
list
|
35
|
+
end
|
36
|
+
|
37
|
+
module CommandStrategy
|
38
|
+
|
39
|
+
def execute(connection, *args)
|
40
|
+
connection.send_command(@name, *args)
|
41
|
+
Response.factory(@name).parse(connection.read_response)
|
42
|
+
rescue EOFError
|
43
|
+
connection.close
|
44
|
+
retry
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
module CommandListStrategy
|
50
|
+
|
51
|
+
def execute(connection, *args, &block)
|
52
|
+
list = List.new
|
53
|
+
yield list
|
54
|
+
|
55
|
+
connection.send_command("command_list_begin")
|
56
|
+
list.map do |command_w_args|
|
57
|
+
connection.send_command(*command_w_args)
|
58
|
+
end
|
59
|
+
connection.send_command("command_list_end")
|
60
|
+
Response.factory(@name).parse(connection.read_response)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
module CommandListOkStrategy
|
66
|
+
|
67
|
+
def execute(connection, *args, &block)
|
68
|
+
@list = List.new
|
69
|
+
yield @list
|
70
|
+
|
71
|
+
connection.send_command("command_list_ok_begin")
|
72
|
+
@list.map do |command_w_args|
|
73
|
+
connection.send_command(*command_w_args)
|
74
|
+
end
|
75
|
+
connection.send_command("command_list_end")
|
76
|
+
handle_command_list_ok_response(connection.read_response)
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def handle_command_list_ok_response(lines)
|
83
|
+
lines.pop while lines.last =~ LIST_OK_RE || lines.last =~ OK_RE
|
84
|
+
ResponseArray.new(split_responses(lines))
|
85
|
+
end
|
86
|
+
|
87
|
+
def split_responses(lines)
|
88
|
+
commands = @list.map(&:first)
|
89
|
+
|
90
|
+
lines.reduce([[]]) do |ra, line|
|
91
|
+
if LIST_OK_RE === line
|
92
|
+
ra << []
|
93
|
+
else
|
94
|
+
ra.last << line
|
95
|
+
end
|
96
|
+
ra
|
97
|
+
end.map do |response|
|
98
|
+
Response.factory(commands.shift).parse(response)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
class ResponseArray < DelegateClass(Array)
|
105
|
+
|
106
|
+
def ok?
|
107
|
+
all?(&:ok?)
|
108
|
+
end
|
109
|
+
|
110
|
+
def ack?
|
111
|
+
!ok?
|
112
|
+
end
|
113
|
+
|
114
|
+
def error
|
115
|
+
find {|e| e.ack?}.error if ack?
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
class List
|
121
|
+
extend Forwardable
|
122
|
+
|
123
|
+
def_delegators :@cmds, :empty?, :each, :map, :size
|
124
|
+
|
125
|
+
def initialize
|
126
|
+
@cmds = []
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
def method_missing(name, *args, &block)
|
133
|
+
@cmds << [name.to_s, *args]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class Splitter
|
138
|
+
def initialize(regexp)
|
139
|
+
@regexp = regexp
|
140
|
+
end
|
141
|
+
|
142
|
+
def split(lines)
|
143
|
+
lines.reduce([]) do |c, i|
|
144
|
+
if @regexp === i
|
145
|
+
c << [i]
|
146
|
+
else
|
147
|
+
c.last << i
|
148
|
+
end
|
149
|
+
c
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
data/lib/rmpd/commands.rb
CHANGED
@@ -10,25 +10,53 @@ module Rmpd
|
|
10
10
|
|
11
11
|
private
|
12
12
|
|
13
|
-
def
|
14
|
-
read_response(MultiResponse, regexp)
|
15
|
-
end
|
16
|
-
|
17
|
-
def receive_server_response
|
18
|
-
@last_response = nil
|
13
|
+
def receive_response
|
19
14
|
lines = []
|
15
|
+
|
20
16
|
while lines << @socket.readline do
|
21
|
-
puts "recv: #{lines.last.strip}
|
17
|
+
puts "recv: #{lines.last.strip}" if $DEBUG
|
22
18
|
case lines.last
|
23
|
-
when ACK_RE, OK_RE
|
24
|
-
@last_response = lines.last
|
19
|
+
when ACK_RE, OK_RE
|
25
20
|
break
|
26
21
|
end
|
27
22
|
end
|
28
|
-
|
23
|
+
|
24
|
+
lines
|
29
25
|
end
|
30
26
|
|
31
27
|
def send_command(command, *args)
|
28
|
+
if in_command_list?
|
29
|
+
@command_list << [command, args]
|
30
|
+
else
|
31
|
+
case command
|
32
|
+
when /^command_list_end$/
|
33
|
+
# blah
|
34
|
+
@command_list = nil
|
35
|
+
when /^command_list.*begin$/
|
36
|
+
@command_list = [command, args]
|
37
|
+
else
|
38
|
+
send_command_now(command, *args)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def in_command_list?
|
44
|
+
!@command_list.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def send_command_now(command, *args)
|
48
|
+
connect
|
49
|
+
@socket.puts("#{command} #{args.join(" ")}".strip)
|
50
|
+
rescue Errno::EPIPE, EOFError
|
51
|
+
@socket.close
|
52
|
+
if (tries += 1) < 5
|
53
|
+
retry
|
54
|
+
else
|
55
|
+
raise MpdError.new("Retry count exceeded")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def send_command_old(command, *args)
|
32
60
|
tries = 0
|
33
61
|
|
34
62
|
if $DEBUG
|
@@ -47,6 +75,8 @@ module Rmpd
|
|
47
75
|
raise MpdError.new("Retry count exceeded")
|
48
76
|
end
|
49
77
|
end
|
78
|
+
|
79
|
+
receive_response unless @in_command_list
|
50
80
|
end
|
51
81
|
|
52
82
|
end
|
data/lib/rmpd/commands/admin.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
module Rmpd
|
2
2
|
module Commands
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
simple_command :outputs
|
5
|
+
simple_command :tagtypes, :min_version => [0, 13, 0]
|
7
6
|
simple_command :disableoutput
|
8
7
|
simple_command :enableoutput
|
9
8
|
simple_command :update
|
9
|
+
simple_command :_kill
|
10
10
|
|
11
11
|
def kill
|
12
|
-
|
12
|
+
_kill
|
13
13
|
@socket.close
|
14
14
|
end
|
15
15
|
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module Rmpd
|
2
2
|
module Commands
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
simple_command :count
|
5
|
+
simple_command :find
|
6
|
+
simple_command :list
|
7
|
+
simple_command :listall
|
8
|
+
simple_command :listallinfo
|
9
|
+
simple_command :lsinfo
|
10
|
+
simple_command :search
|
11
11
|
|
12
12
|
alias_method :list_all, :listall
|
13
13
|
alias_method :list_all_info, :listallinfo
|
@@ -4,39 +4,13 @@ module Rmpd
|
|
4
4
|
private
|
5
5
|
|
6
6
|
def self.simple_command(name, args={})
|
7
|
-
|
8
|
-
if args.include?(:min_version)
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
read_response unless @in_command_list
|
13
|
-
end
|
14
|
-
send(:define_method, name, &block)
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.complex_command(name, args={})
|
18
|
-
args = {:regexp => /(^file: )/i}.merge(args)
|
19
|
-
block = lambda do |*a|
|
20
|
-
if args.include?(:min_version)
|
21
|
-
server_version_at_least(*args[:min_version])
|
22
|
-
end
|
23
|
-
send_command(name.to_s.gsub(/^_*/, ""), *quote(a))
|
24
|
-
if @in_command_list
|
25
|
-
append_command_list_regexp(args[:regexp])
|
26
|
-
else
|
27
|
-
read_responses(args[:regexp])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
send(:define_method, name, &block)
|
31
|
-
end
|
32
|
-
|
33
|
-
def append_command_list_regexp(regexp)
|
34
|
-
if @in_command_list_response_regexp
|
35
|
-
@in_command_list_response_regexp = \
|
36
|
-
Regexp.union(@in_command_list_response_regexp, regexp)
|
37
|
-
else
|
38
|
-
@in_command_list_response_regexp = regexp
|
7
|
+
command = Proc.new do |*a, &block|
|
8
|
+
# if args.include?(:min_version)
|
9
|
+
# server_version_at_least(*args[:min_version])
|
10
|
+
# end
|
11
|
+
Command.new(name.to_s.gsub(/^_*/, "")).execute(mpd, *a, &block)
|
39
12
|
end
|
13
|
+
send(:define_method, name, &command)
|
40
14
|
end
|
41
15
|
|
42
16
|
def quote(args)
|
@@ -1,9 +1,8 @@
|
|
1
1
|
module Rmpd
|
2
2
|
module Commands
|
3
3
|
|
4
|
-
|
5
|
-
simple_command :notcommands
|
6
|
-
|
4
|
+
simple_command :commands
|
5
|
+
simple_command :notcommands
|
7
6
|
simple_command :clearerror
|
8
7
|
simple_command :idle
|
9
8
|
simple_command :noidle
|
@@ -11,60 +10,18 @@ module Rmpd
|
|
11
10
|
simple_command :ping
|
12
11
|
simple_command :stats
|
13
12
|
simple_command :status
|
13
|
+
simple_command :_close
|
14
14
|
|
15
15
|
def close
|
16
|
-
|
16
|
+
_close
|
17
17
|
@socket.close
|
18
18
|
end
|
19
19
|
|
20
|
+
simple_command :command_list
|
21
|
+
simple_command :command_list_ok
|
22
|
+
|
20
23
|
alias_method :clear_error, :clearerror
|
21
24
|
alias_method :not_commands, :notcommands
|
22
25
|
|
23
|
-
def command_list
|
24
|
-
send_command("command_list_begin")
|
25
|
-
@in_command_list = true
|
26
|
-
yield self
|
27
|
-
send_command("command_list_end")
|
28
|
-
handle_command_list_response
|
29
|
-
ensure
|
30
|
-
@in_command_list = false
|
31
|
-
@in_command_list_response_regexp = nil
|
32
|
-
end
|
33
|
-
|
34
|
-
def command_list_ok
|
35
|
-
send_command("command_list_ok_begin")
|
36
|
-
@in_command_list = true
|
37
|
-
yield self
|
38
|
-
send_command("command_list_end")
|
39
|
-
read_command_list_ok_responses do |responses|
|
40
|
-
handle_command_list_response.tap do |res|
|
41
|
-
responses << res unless res.empty?
|
42
|
-
end
|
43
|
-
end
|
44
|
-
ensure
|
45
|
-
@in_command_list = false
|
46
|
-
@in_command_list_response_regexp = nil
|
47
|
-
end
|
48
|
-
|
49
|
-
|
50
|
-
private
|
51
|
-
|
52
|
-
def handle_command_list_response
|
53
|
-
if @in_command_list_response_regexp
|
54
|
-
read_responses(@in_command_list_response_regexp)
|
55
|
-
else
|
56
|
-
read_response
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
-
def read_command_list_ok_responses
|
61
|
-
responses = []
|
62
|
-
|
63
|
-
begin
|
64
|
-
yield responses
|
65
|
-
end while LIST_OK_RE === @last_response
|
66
|
-
|
67
|
-
responses
|
68
|
-
end
|
69
26
|
end
|
70
27
|
end
|
@@ -1,14 +1,14 @@
|
|
1
1
|
module Rmpd
|
2
2
|
module Commands
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
4
|
+
simple_command :listplaylist, :min_version => [0, 13, 0]
|
5
|
+
simple_command :listplaylistinfo, :min_version => [0, 12, 0]
|
6
|
+
simple_command :playlistfind, :min_version => [0, 13, 0]
|
7
|
+
simple_command :playlistid
|
8
|
+
simple_command :playlistinfo
|
9
|
+
simple_command :playlistsearch, :min_version => [0, 13, 0]
|
10
|
+
simple_command :plchanges
|
11
|
+
simple_command :plchangesposid
|
12
12
|
|
13
13
|
simple_command :_playlist
|
14
14
|
simple_command :add # The docs on the wiki don't line up with empirical
|
@@ -30,22 +30,7 @@ module Rmpd
|
|
30
30
|
simple_command :shuffle
|
31
31
|
simple_command :swap
|
32
32
|
simple_command :swapid
|
33
|
-
|
34
|
-
|
35
|
-
# must be a file only, cannot use with a directory
|
36
|
-
def addid(path, pos=nil)
|
37
|
-
# pos is only for r7153+, but what version is that?
|
38
|
-
server_version_at_least(0, 14, 0) if pos
|
39
|
-
args = [path]
|
40
|
-
args << pos if pos
|
41
|
-
send_command("addid", *quote(args))
|
42
|
-
@add_id_response_regex ||= /(^Id: )/i
|
43
|
-
if @in_command_list
|
44
|
-
append_command_list_regexp(@add_id_response_regex)
|
45
|
-
else
|
46
|
-
read_responses(@add_id_response_regex)
|
47
|
-
end
|
48
|
-
end
|
33
|
+
simple_command :addid
|
49
34
|
|
50
35
|
alias_method :add_id, :addid
|
51
36
|
alias_method :current_song, :currentsong
|
data/lib/rmpd/config.rb
CHANGED
@@ -21,7 +21,8 @@ module Rmpd
|
|
21
21
|
else
|
22
22
|
config = {}
|
23
23
|
end
|
24
|
-
|
24
|
+
puts "env: #{detected_env}" if env_detected? && $DEBUG
|
25
|
+
config = config[detected_env] if env_detected?
|
25
26
|
puts "config: #{config.inspect}" if $DEBUG
|
26
27
|
init_host_and_password(config)
|
27
28
|
init_port(config)
|
@@ -33,6 +34,22 @@ module Rmpd
|
|
33
34
|
HOSTNAME_RE = /(.*)@(.*)/
|
34
35
|
|
35
36
|
|
37
|
+
def detected_env
|
38
|
+
if defined?(Rails)
|
39
|
+
Rails.env
|
40
|
+
elsif ENV.include?("APP_ENV")
|
41
|
+
ENV["APP_ENV"]
|
42
|
+
elsif ENV.include?("RACK_ENV")
|
43
|
+
ENV["RACK_ENV"]
|
44
|
+
elsif ENV.include?("RAILS_ENV")
|
45
|
+
ENV["RAILS_ENV"]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def env_detected?
|
50
|
+
!!detected_env
|
51
|
+
end
|
52
|
+
|
36
53
|
def init_host_and_password(config)
|
37
54
|
if config["hostname"]
|
38
55
|
@hostname = parse_hostname(config["hostname"])
|
data/lib/rmpd/connection.rb
CHANGED
@@ -3,81 +3,78 @@ require "socket"
|
|
3
3
|
|
4
4
|
module Rmpd
|
5
5
|
class Connection
|
6
|
-
|
7
|
-
include Rmpd::Commands
|
8
6
|
include Socket::Constants
|
7
|
+
include Rmpd::Commands
|
9
8
|
|
10
|
-
attr_reader :error
|
11
9
|
|
12
|
-
|
13
|
-
@config = Rmpd::Config.new(config_file)
|
14
|
-
@socket = nil
|
15
|
-
end
|
10
|
+
MAX_RETRIES = 5
|
16
11
|
|
17
|
-
def server_version
|
18
|
-
"#{@server_version_major}.#{@server_version_minor}.#{@server_version_patch}"
|
19
|
-
end
|
20
12
|
|
21
13
|
attr_reader :socket
|
22
14
|
|
23
|
-
private
|
24
15
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
16
|
+
def initialize(config_file=nil)
|
17
|
+
@config = Rmpd::Config.new(config_file)
|
18
|
+
@socket = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def close
|
22
|
+
@socket.close
|
30
23
|
end
|
31
24
|
|
32
25
|
def connect
|
33
26
|
return unless @socket.nil? || @socket.closed?
|
34
|
-
error = nil
|
35
27
|
|
36
28
|
Socket::getaddrinfo(@config.hostname, @config.port, nil, SOCK_STREAM).each do |info|
|
37
29
|
begin
|
38
|
-
puts "args: #{info.inspect}" if $DEBUG
|
39
30
|
sockaddr = Socket.pack_sockaddr_in(info[1], info[3])
|
40
31
|
@socket = Socket.new(info[4], info[5], 0)
|
41
32
|
@socket.connect(sockaddr)
|
42
33
|
rescue StandardError => error
|
43
34
|
$stderr.puts "Failed to connect to #{info[3]}: #{error}"
|
44
35
|
@socket = nil
|
36
|
+
raise MpdConnRefusedError.new(error)
|
45
37
|
else
|
46
38
|
break
|
47
39
|
end
|
48
40
|
end
|
49
|
-
raise MpdConnRefusedError.new(error) if @socket.nil?
|
50
41
|
|
51
|
-
|
52
|
-
|
42
|
+
read_response # protocol version, ignore for now
|
43
|
+
password(@config.password) if @config.password
|
53
44
|
end
|
54
45
|
|
55
|
-
def
|
56
|
-
|
57
|
-
@server_version_major = $~[1].to_i
|
58
|
-
@server_version_minor = $~[2].to_i
|
59
|
-
@server_version_patch = $~[3].to_i
|
60
|
-
end
|
46
|
+
def send_command(command, *args)
|
47
|
+
tries = 0
|
61
48
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
49
|
+
begin
|
50
|
+
connect
|
51
|
+
@socket.puts("#{command} #{quote(args).join(" ")}".strip)
|
52
|
+
rescue Errno::EPIPE, EOFError
|
53
|
+
@socket.close
|
54
|
+
if (tries += 1) < MAX_RETRIES
|
55
|
+
retry
|
56
|
+
else
|
57
|
+
raise MpdError.new("Retry count exceeded")
|
58
|
+
end
|
59
|
+
end
|
66
60
|
end
|
67
61
|
|
68
|
-
def
|
69
|
-
|
70
|
-
e = MpdError.new("Requires server version #{major}.#{minor}.#{patch}")
|
62
|
+
def read_response
|
63
|
+
response = []
|
71
64
|
|
72
|
-
|
73
|
-
|
65
|
+
while (line = @socket.readline)
|
66
|
+
response << line.strip
|
67
|
+
break if END_RE === line
|
68
|
+
end
|
69
|
+
response
|
70
|
+
end
|
74
71
|
|
75
|
-
|
76
|
-
|
72
|
+
def mpd
|
73
|
+
self
|
74
|
+
end
|
77
75
|
|
78
|
-
|
79
|
-
|
80
|
-
true
|
76
|
+
def quote(args)
|
77
|
+
args.collect {|arg| "\"#{arg.to_s.gsub(/"/, "\\\"")}\""}
|
81
78
|
end
|
82
79
|
|
83
80
|
end
|
data/lib/rmpd/response.rb
CHANGED
@@ -1,20 +1,72 @@
|
|
1
1
|
require "delegate"
|
2
2
|
|
3
3
|
module Rmpd
|
4
|
+
class Response < SimpleDelegator
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
KEY_VALUE_RE = /^([^:]+):\s*(.*)$/
|
7
|
+
KNOWN_INT_FIELDS = [
|
8
|
+
"bitrate",
|
9
|
+
"consume",
|
10
|
+
"id",
|
11
|
+
"nextsong",
|
12
|
+
"nextsongid",
|
13
|
+
"playlist",
|
14
|
+
"playlistlength",
|
15
|
+
"playtime",
|
16
|
+
"pos",
|
17
|
+
"queued",
|
18
|
+
"random",
|
19
|
+
"repeat",
|
20
|
+
"single",
|
21
|
+
"song",
|
22
|
+
"songid",
|
23
|
+
"songs",
|
24
|
+
"track",
|
25
|
+
"volume",
|
26
|
+
"xfade",
|
27
|
+
]
|
28
|
+
KNOWN_FLOAT_FIELDS = [
|
29
|
+
"elapsed",
|
30
|
+
"mixrampdb",
|
31
|
+
]
|
32
|
+
KNOWN_COMPLEX_FIELDS = [
|
33
|
+
"time",
|
34
|
+
"audio"
|
35
|
+
]
|
36
|
+
MULTI_RESPONSE_COMMANDS = [
|
37
|
+
"commands",
|
38
|
+
"find",
|
39
|
+
"idle",
|
40
|
+
"list",
|
41
|
+
"outputs",
|
42
|
+
"playlistinfo",
|
43
|
+
"search",
|
44
|
+
"tagtypes",
|
45
|
+
]
|
9
46
|
|
10
|
-
class Response < DelegateClass(Hash)
|
11
47
|
|
12
48
|
attr_reader :error
|
13
49
|
|
14
|
-
|
15
|
-
|
50
|
+
|
51
|
+
def self.choose_strategy(name)
|
52
|
+
if MULTI_RESPONSE_COMMANDS.include?(name.to_s)
|
53
|
+
[Array, ResponseMultiStrategy]
|
54
|
+
else
|
55
|
+
[Hash, ResponseSingleStrategy]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.factory(command_name)
|
60
|
+
if MULTI_RESPONSE_COMMANDS.include?(command_name.to_s)
|
61
|
+
MultiResponse.new
|
62
|
+
else
|
63
|
+
SingleResponse.new
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def initialize(parent)
|
68
|
+
super
|
16
69
|
@error = nil
|
17
|
-
parse(data)
|
18
70
|
end
|
19
71
|
|
20
72
|
def ok?
|
@@ -25,24 +77,32 @@ module Rmpd
|
|
25
77
|
!ok?
|
26
78
|
end
|
27
79
|
|
28
|
-
|
29
|
-
|
30
|
-
def register_key_val_pair(r)
|
31
|
-
key, val = r[1].downcase.to_sym, r[2]
|
32
|
-
val = val.to_i if KNOWN_INT_FIELDS.include?(key)
|
33
|
-
val = send("parse_complex_#{key}", val) if KNOWN_COMPLEX_FIELDS.include?(key)
|
34
|
-
self[key] = include?(key) ? ([self[key]] << val).flatten : val
|
35
|
-
self.class.send(:define_method, key) {self[key]}
|
36
|
-
end
|
37
|
-
|
38
|
-
def parse(data)
|
39
|
-
data.split("\n").each do |line|
|
80
|
+
def parse(lines)
|
81
|
+
lines.each do |line|
|
40
82
|
case line
|
41
|
-
when KEY_VALUE_RE
|
42
|
-
|
43
|
-
when ACK_RE
|
83
|
+
when KEY_VALUE_RE
|
84
|
+
register_key_val_pair($~)
|
85
|
+
when ACK_RE
|
86
|
+
@error = $~[0]
|
87
|
+
break
|
88
|
+
when OK_RE
|
89
|
+
break
|
90
|
+
else
|
91
|
+
$stderr.puts "Don't know how to parse: #{line}"
|
44
92
|
end
|
45
93
|
end
|
94
|
+
|
95
|
+
self
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def transform_value(key, val)
|
102
|
+
val = val.to_i if KNOWN_INT_FIELDS.include?(key)
|
103
|
+
val = val.to_f if KNOWN_FLOAT_FIELDS.include?(key)
|
104
|
+
val = send("parse_complex_#{key}", val) if KNOWN_COMPLEX_FIELDS.include?(key.to_s)
|
105
|
+
val
|
46
106
|
end
|
47
107
|
|
48
108
|
# time can be either an integer (playlistinfo) or elapsed:total (status)
|
@@ -54,5 +114,56 @@ module Rmpd
|
|
54
114
|
end
|
55
115
|
end
|
56
116
|
|
117
|
+
def parse_complex_audio(value)
|
118
|
+
value.split(":", 3).map(&:to_i)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
class MultiResponse < Response
|
124
|
+
|
125
|
+
def initialize
|
126
|
+
super([])
|
127
|
+
end
|
128
|
+
|
129
|
+
def parse(lines)
|
130
|
+
@first_key = nil
|
131
|
+
@temp = {}
|
132
|
+
|
133
|
+
super(lines)
|
134
|
+
self << @temp unless @temp.empty?
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
def register_key_val_pair(match_data)
|
139
|
+
key = match_data[1].downcase
|
140
|
+
val = transform_value(key, match_data[2])
|
141
|
+
|
142
|
+
if @first_key == key
|
143
|
+
self << @temp
|
144
|
+
@temp = {key => val}
|
145
|
+
else
|
146
|
+
@first_key ||= key
|
147
|
+
@temp[key] = val
|
148
|
+
@temp.class.send(:define_method, key.to_s.gsub(/-/, "_")) {self[key]}
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
class SingleResponse < Response
|
155
|
+
|
156
|
+
def initialize
|
157
|
+
super({})
|
158
|
+
end
|
159
|
+
|
160
|
+
def register_key_val_pair(match_data)
|
161
|
+
key = match_data[1].downcase
|
162
|
+
val = transform_value(key, match_data[2])
|
163
|
+
|
164
|
+
self[key] = include?(key) ? ([self[key]] << val).flatten : val
|
165
|
+
self.class.send(:define_method, key.to_s.gsub(/-/,"_")) {self[key]}
|
166
|
+
end
|
167
|
+
|
57
168
|
end
|
58
169
|
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Rmpd
|
2
|
+
class ResponseSplitter
|
3
|
+
|
4
|
+
def self.split(lines, responses=[])
|
5
|
+
known_keys = []
|
6
|
+
response_lines = []
|
7
|
+
|
8
|
+
lines.each do |line|
|
9
|
+
if KEY_VALUE_RE === line
|
10
|
+
key, value = $~.values_at(1..2)
|
11
|
+
if known_keys.include?(key)
|
12
|
+
yield responses, response_lines
|
13
|
+
response_lines.clear
|
14
|
+
known_keys.clear
|
15
|
+
end
|
16
|
+
|
17
|
+
response_lines << line
|
18
|
+
known_keys << key
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
yield responses, response_lines
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
data/lib/rmpd/version.rb
CHANGED
data/rmpd.gemspec
CHANGED
@@ -33,16 +33,6 @@ describe Rmpd::Commands do
|
|
33
33
|
end.should raise_error(Rmpd::MpdError, "Retry count exceeded")
|
34
34
|
end
|
35
35
|
|
36
|
-
it "should hide password output when in debug mode" do
|
37
|
-
set_password
|
38
|
-
@socket.stub!(:readline).and_return(*(connect_and_auth_responses + ok))
|
39
|
-
Kernel.should_receive(:puts).with("send: password #{"*"*8}")
|
40
|
-
Kernel.should_receive(:puts).with("send: ping")
|
41
|
-
$DEBUG = 1
|
42
|
-
@conn.ping
|
43
|
-
$DEBUG = nil
|
44
|
-
end
|
45
|
-
|
46
36
|
it "should abort after a limited number of tries against a closed connection" do
|
47
37
|
@socket.stub!(:puts).and_raise(EOFError)
|
48
38
|
@socket.stub!(:readline).and_return(*(connect_response + ok))
|
data/spec/models/config_spec.rb
CHANGED
@@ -25,6 +25,10 @@ describe Rmpd::Config do
|
|
25
25
|
@filename = File.join(File.dirname(__FILE__), "../../spec/fixtures/config_rails.yml")
|
26
26
|
end
|
27
27
|
|
28
|
+
after(:each) do
|
29
|
+
Object.send(:remove_const, :Rails) if defined?(Rails)
|
30
|
+
end
|
31
|
+
|
28
32
|
it "should load the development environment" do
|
29
33
|
lambda do
|
30
34
|
Rmpd::Config.new(@filename)
|
@@ -29,6 +29,7 @@ describe Connection do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it "should generate a server version" do
|
32
|
+
pending "Do I care anymore?"
|
32
33
|
version = "0.1.2"
|
33
34
|
responses = connect_response(version) + ok
|
34
35
|
@socket.should_receive(:readline).and_return(*responses)
|
@@ -44,6 +45,7 @@ describe Connection do
|
|
44
45
|
end
|
45
46
|
|
46
47
|
it "should restrict access based on server version" do
|
48
|
+
pending "Do I care anymore?"
|
47
49
|
responses = connect_response("0.1.0")
|
48
50
|
@socket.stub!(:readline).and_return(*responses)
|
49
51
|
@conn.instance_eval do
|
@@ -4,6 +4,7 @@ module Rmpd
|
|
4
4
|
describe Commands, "Generators" do
|
5
5
|
|
6
6
|
before(:each) do
|
7
|
+
pending "Do I care anymore?"
|
7
8
|
@config = mock_config
|
8
9
|
@socket = mock_socket
|
9
10
|
@conn = Connection.new
|
@@ -19,21 +20,13 @@ module Rmpd
|
|
19
20
|
|
20
21
|
describe "simple_command" do
|
21
22
|
it "should allow based on server version" do
|
23
|
+
pending "Do I care anymore?"
|
22
24
|
Commands::simple_command(:test, :min_version => [0, 1, 1])
|
23
25
|
lambda do
|
24
26
|
@conn.test
|
25
27
|
end.should_not raise_error(MpdError)
|
26
28
|
end
|
27
29
|
end
|
28
|
-
|
29
|
-
describe "complex_command" do
|
30
|
-
it "should allow based on server version" do
|
31
|
-
Rmpd::Commands::complex_command(:test, :min_version => [0, 1, 1])
|
32
|
-
lambda do
|
33
|
-
@conn.test
|
34
|
-
end.should_not raise_error(MpdError)
|
35
|
-
end
|
36
|
-
end
|
37
30
|
end
|
38
31
|
|
39
32
|
describe "when version is NOT ok" do
|
@@ -5,11 +5,8 @@ include Rmpd
|
|
5
5
|
|
6
6
|
describe Rmpd::Response do
|
7
7
|
before(:each) do
|
8
|
-
data =
|
9
|
-
|
10
|
-
OK
|
11
|
-
EOF
|
12
|
-
@response = Rmpd::Response.new(data)
|
8
|
+
data = ["foo: bar", "OK"]
|
9
|
+
@response = Rmpd::Response.factory("status").parse(data)
|
13
10
|
end
|
14
11
|
|
15
12
|
it "should have a foo method" do
|
@@ -18,8 +15,8 @@ EOF
|
|
18
15
|
end
|
19
16
|
|
20
17
|
it "should have a foo key" do
|
21
|
-
@response.should include(
|
22
|
-
@response[
|
18
|
+
@response.should include("foo")
|
19
|
+
@response["foo"].should == "bar"
|
23
20
|
end
|
24
21
|
|
25
22
|
it "should be ok" do
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rmpd
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.5
|
10
|
+
version: 1.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Eric Wollesen
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-
|
18
|
+
date: 2012-09-04 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rspec
|
@@ -61,6 +61,20 @@ dependencies:
|
|
61
61
|
type: :development
|
62
62
|
requirement: *id003
|
63
63
|
prerelease: false
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: pry
|
66
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
type: :development
|
76
|
+
requirement: *id004
|
77
|
+
prerelease: false
|
64
78
|
description: Music Player Daemon client in Ruby
|
65
79
|
email:
|
66
80
|
- ericw@xmtp.net
|
@@ -76,7 +90,9 @@ files:
|
|
76
90
|
- .rvmrc
|
77
91
|
- Gemfile
|
78
92
|
- Rakefile
|
93
|
+
- console.rb
|
79
94
|
- lib/rmpd.rb
|
95
|
+
- lib/rmpd/command.rb
|
80
96
|
- lib/rmpd/commands.rb
|
81
97
|
- lib/rmpd/commands/admin.rb
|
82
98
|
- lib/rmpd/commands/database.rb
|
@@ -86,8 +102,8 @@ files:
|
|
86
102
|
- lib/rmpd/commands/playlist.rb
|
87
103
|
- lib/rmpd/config.rb
|
88
104
|
- lib/rmpd/connection.rb
|
89
|
-
- lib/rmpd/multi_response.rb
|
90
105
|
- lib/rmpd/response.rb
|
106
|
+
- lib/rmpd/response_splitter.rb
|
91
107
|
- lib/rmpd/version.rb
|
92
108
|
- rmpd.gemspec
|
93
109
|
- spec/fixtures/config.yml
|
@@ -100,7 +116,6 @@ files:
|
|
100
116
|
- spec/models/connection_spec.rb
|
101
117
|
- spec/models/generators_spec.rb
|
102
118
|
- spec/models/mildred_mpd_spec.rb
|
103
|
-
- spec/models/multi_response_spec.rb
|
104
119
|
- spec/models/response_spec.rb
|
105
120
|
- spec/spec.opts
|
106
121
|
- spec/spec_helper.rb
|
data/lib/rmpd/multi_response.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
require "delegate"
|
2
|
-
|
3
|
-
module Rmpd
|
4
|
-
|
5
|
-
class MultiResponse < DelegateClass(Array)
|
6
|
-
|
7
|
-
END_OF_RECORD = "\0x1E" # from ASCII
|
8
|
-
|
9
|
-
def initialize(data, sep)
|
10
|
-
super([])
|
11
|
-
@sep = sep
|
12
|
-
parse(data)
|
13
|
-
end
|
14
|
-
|
15
|
-
def ok?
|
16
|
-
self.all?(&:ok?)
|
17
|
-
end
|
18
|
-
|
19
|
-
def ack?
|
20
|
-
!ok?
|
21
|
-
end
|
22
|
-
|
23
|
-
def error
|
24
|
-
self.map(&:error).compact.first
|
25
|
-
end
|
26
|
-
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def parse(data)
|
31
|
-
data.gsub!(@sep, "#{END_OF_RECORD}\\1")
|
32
|
-
data.split(END_OF_RECORD).each do |datum|
|
33
|
-
if datum.empty?
|
34
|
-
next
|
35
|
-
else
|
36
|
-
r = Response.new(datum)
|
37
|
-
end
|
38
|
-
self << r unless r.keys.size.zero? && r.ok?
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
@@ -1,54 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
include Rmpd
|
4
|
-
|
5
|
-
|
6
|
-
describe Rmpd::MultiResponse do
|
7
|
-
describe "on success" do
|
8
|
-
before(:each) do
|
9
|
-
data = <<-EOF
|
10
|
-
foo: cat
|
11
|
-
bar: dog
|
12
|
-
foo: horse
|
13
|
-
bar: giraffe
|
14
|
-
OK
|
15
|
-
EOF
|
16
|
-
@response = Rmpd::MultiResponse.new(data, /(^foo:)/)
|
17
|
-
end
|
18
|
-
|
19
|
-
it "should have a size of 2" do
|
20
|
-
@response.should have(2).items, @response.inspect
|
21
|
-
end
|
22
|
-
|
23
|
-
it "should be OK" do
|
24
|
-
# @response.should be_ok
|
25
|
-
# doesn't work correctly, see rspec bug #11526
|
26
|
-
@response.ok?.should be_true
|
27
|
-
end
|
28
|
-
|
29
|
-
it "should not be ACK" do
|
30
|
-
# @response.should_not be_ack
|
31
|
-
# doesn't work correctly, see rspec bug #11526
|
32
|
-
@response.ack?.should be_false
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
describe "on error" do
|
37
|
-
before(:each) do
|
38
|
-
@err_msg = "ACK [2@0] {search} too few arguments for \"search\"\n"
|
39
|
-
@response = Rmpd::MultiResponse.new(@err_msg, /(^foo:)/)
|
40
|
-
end
|
41
|
-
|
42
|
-
it "should not be OK" do
|
43
|
-
# @response.should_not be_ok
|
44
|
-
# doesn't work correctly, see rspec bug #11526
|
45
|
-
@response.ok?.should be_false
|
46
|
-
end
|
47
|
-
|
48
|
-
it "should be ACK" do
|
49
|
-
# @response.should be_ack
|
50
|
-
# doesn't work correctly, see rspec bug #11526
|
51
|
-
@response.ack?.should be_true
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|