1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
| | # Copyright (C) 2015-2016 all contributors <dtas-all@nongnu.org>
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
# frozen_string_literal: true
# emulate the MPD protocol
require_relative '../dtas'
require 'shellwords'
class DTAS::MpdEmuClient # :nodoc:
attr_reader :to_io
# protocol version we support
SERVER = 'MPD 0.13.0'
MAX_RBUF = 8192
ACK = {
ERROR_NOT_LIST: 1,
ERROR_ARG: 2,
ERROR_PASSWORD: 3,
ERROR_PERMISSION: 4,
ERROR_UNKNOWN: 5,
ERROR_NO_EXIST: 50,
ERROR_PLAYLIST_MAX: 51,
ERROR_SYSTEM: 52,
ERROR_PLAYLIST_LOAD: 53,
ERROR_UPDATE_ALREADY: 54,
ERROR_PLAYER_SYNC: 55,
ERROR_EXIST: 56,
}
def initialize(io)
@to_io = io
@rbuf = ''.b
@wbuf = nil
@cmd_listnum = 0
@cmd_list = nil
out("OK #{SERVER}\n")
end
def dispatch_loop(rbuf)
while rbuf.sub!(/\A([^\r\n]+)\r?\n/n, '')
rv = dispatch(Shellwords.split($1))
next if rv == true
return rv
end
rbuf.size >= MAX_RBUF ? nil : :wait_readable
end
def dispatch(argv)
cmd = argv.shift or return err(:ERROR_UNKNOWN)
cmd = "mpdcmd_#{cmd}"
if respond_to?(cmd)
m = method(cmd)
params = m.parameters
rest = params.any? { |x| x[0] == :rest }
req = params.count { |x| x[0] == :req }
opt = params.count { |x| x[0] == :opt }
argc = argv.size
return err(:ERROR_ARG) if argc < req
return err(:ERROR_ARG) if !rest && (argc > (req + opt))
m.call(*argv)
else
err(:ERROR_UNKNOWN)
end
end
def unknown_cmd(cmd)
%Q!#{err(:ERROR_UNKNOWN)} unknown command "#{cmd}"\n!
end
def err(sym)
"[#{ACK[sym]}@#@cmd_listnum {}"
end
def mpdcmd_ping; out("OK\n"); end
def mpdcmd_close(*); nil; end
def mpdcmd_clearerror
# player_clear_error
out("OK\n")
end
def mpdcmd_command_list_begin
if @cmd_list
@cmd_list << 'command_list_begin' # will trigger failure
else
@cmd_list = []
end
end
def mpdcmd_command_list_end
@cmd_list or return out(unknown_cmd('command_list_end'))
list = @cmd_list
@cmd_list = nil
list.each do |cmd|
case cmd
when 'command_list_begin', 'command_list_ok_begin'
return out(unknown_cmd(cmd))
end
end
end
def mpdcmd_stats
out("artists: \n" \
"albums: \n" \
"songs: \n" \
"uptime: \n" \
"playtime: \n" \
"db_playtime: \n" \
"db_update: \n" \
"OK\n")
end
# returns true on complete, :wait_writable when blocked, or nil on error
def out(buf)
buf = buf.b
if @wbuf
@wbuf << buf
:wait_writable
else
tot = buf.size
case rv = @to_io.write_nonblock(buf, exception: false)
when Integer
return true if rv == tot
buf.slice!(0, rv).clear
tot -= rv
when :wait_writable
@wbuf = buf.dup
return rv
end while tot > 0
true # all done
end
rescue
nil # signal EOF up the chain
end
def dispatch_rd(buf)
case rv = @to_io.read_nonblock(MAX_RBUF, buf, exception: false)
when String then dispatch_loop(@rbuf << rv)
when :wait_readable, nil then rv
end
rescue
nil
end
def dispatch_wr
tot = @wbuf.size
case rv = @to_io.write_nonblock(@wbuf, exception: false)
when Integer
@wbuf.slice!(0, rv).clear # free the written portion
tot -= rv
return :wait_readable if tot == 0
when :wait_writable then return rv
end while true
rescue
nil
end
def hash
@to_io.fileno
end
end
|