rmpd 1.0.5 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|