vlc-client 0.0.1.beta
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/.gitignore +21 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +108 -0
- data/Rakefile +40 -0
- data/lib/vlc-client.rb +102 -0
- data/lib/vlc-client/client/connection_management.rb +23 -0
- data/lib/vlc-client/client/media_controls.rb +98 -0
- data/lib/vlc-client/client/video_controls.rb +10 -0
- data/lib/vlc-client/connection.rb +77 -0
- data/lib/vlc-client/core_ext/array.rb +12 -0
- data/lib/vlc-client/errors.rb +16 -0
- data/lib/vlc-client/null_object.rb +24 -0
- data/lib/vlc-client/server.rb +73 -0
- data/lib/vlc-client/system.rb +58 -0
- data/lib/vlc-client/version.rb +3 -0
- data/spec/helper.rb +40 -0
- data/spec/system_spec.rb +31 -0
- data/spec/vlc-client/client/connection_management_spec.rb +24 -0
- data/spec/vlc-client/client/media_controls_spec.rb +188 -0
- data/spec/vlc-client/client/video_controls_spec.rb +13 -0
- data/spec/vlc-client/connection_spec.rb +70 -0
- data/spec/vlc-client/server_spec.rb +19 -0
- data/spec/vlc_client_spec.rb +42 -0
- data/vlc.gemspec +29 -0
- metadata +159 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module VLC
|
2
|
+
#@ private
|
3
|
+
#
|
4
|
+
# Manages the connection to a VLC server
|
5
|
+
#
|
6
|
+
class Connection
|
7
|
+
attr_accessor :host, :port
|
8
|
+
|
9
|
+
def initialize(host, port)
|
10
|
+
@host, @port = host, port
|
11
|
+
@socket = NullObject.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Connects to VLC RC interface on Client#host and Client#port
|
15
|
+
def connect
|
16
|
+
@socket = TCPSocket.new(@host, @port)
|
17
|
+
2.times { read } #Clean the reading channel
|
18
|
+
true
|
19
|
+
rescue Errno::ECONNREFUSED => e
|
20
|
+
raise VLC::ConnectionRefused, "Could not connect to #{@host}:#{@port}: #{e}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Queries if there is a connection to VLC RC interface
|
24
|
+
#
|
25
|
+
# @return [Boolean] true is connected, false otherwise
|
26
|
+
#
|
27
|
+
def connected?
|
28
|
+
not @socket.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Disconnects from VLC RC interface
|
32
|
+
def disconnect
|
33
|
+
@socket.close
|
34
|
+
@socket = NullObject.new
|
35
|
+
end
|
36
|
+
|
37
|
+
alias :close :disconnect
|
38
|
+
|
39
|
+
# Writes data to the TCP server socket
|
40
|
+
#
|
41
|
+
# @param data the data to write
|
42
|
+
# @param fire_and_forget if true, no response response is expected from server,
|
43
|
+
# when false, a response from the server will be returned.
|
44
|
+
#
|
45
|
+
# @return the server response data if there is one
|
46
|
+
#
|
47
|
+
def write(data, fire_and_forget = true)
|
48
|
+
raise NotConnectedError, "no connection to server" unless connected?
|
49
|
+
@socket.puts(data)
|
50
|
+
@socket.flush
|
51
|
+
|
52
|
+
return true if fire_and_forget
|
53
|
+
read
|
54
|
+
rescue Errno::EPIPE
|
55
|
+
disconnect
|
56
|
+
raise BrokenConnectionError, "the connection to the server is lost"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Reads data from the TCP server
|
60
|
+
#
|
61
|
+
# @return [String] the data
|
62
|
+
#
|
63
|
+
def read
|
64
|
+
#TODO: Timeouts
|
65
|
+
raw_data = @socket.gets.chomp
|
66
|
+
if (data = process_data(raw_data))
|
67
|
+
data[1]
|
68
|
+
else
|
69
|
+
raise ProtocolError, "could not interpret the playload: #{raw_data}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_data(data)
|
74
|
+
data.match(/^[>*\s*]*(.*)$/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module VLC
|
2
|
+
# Root error class
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Raised on connection refusal
|
6
|
+
class ConnectionRefused < Error; end
|
7
|
+
|
8
|
+
# Raised on communication errors
|
9
|
+
class ProtocolError < Error; end
|
10
|
+
|
11
|
+
# Raised on a write to a broken connection
|
12
|
+
class BrokenConnectionError < Error; end
|
13
|
+
|
14
|
+
# Raised on a write ti a disconnected connection
|
15
|
+
class NotConnectedError < Error; end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module VLC
|
2
|
+
# @private
|
3
|
+
class NullObject
|
4
|
+
class << self
|
5
|
+
def Null?(value)
|
6
|
+
value.nil? ? NullObject.new : value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def nil?; true; end
|
11
|
+
def blank?; true; end
|
12
|
+
def empty?; true; end
|
13
|
+
def present?; false; end
|
14
|
+
def size; 0; end
|
15
|
+
def to_a; []; end
|
16
|
+
def to_s; ""; end
|
17
|
+
def to_f; 0.0; end
|
18
|
+
def to_i; 0; end
|
19
|
+
def extract_options!; {}; end
|
20
|
+
def inspect; self.class; end
|
21
|
+
|
22
|
+
def method_missing(*args, &block) self; end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module VLC
|
2
|
+
# Manages a local VLC server in a child process
|
3
|
+
class Server
|
4
|
+
attr_accessor :host, :port, :headless
|
5
|
+
alias :headless? :headless
|
6
|
+
|
7
|
+
#
|
8
|
+
# Creates a VLC server lifecycle manager
|
9
|
+
#
|
10
|
+
# @param [String] host The ip to bind to
|
11
|
+
# @param [Integer] port the port
|
12
|
+
# @param [Boolean] headless if true VLC media player will run headless.
|
13
|
+
# i.e. without a graphical interface. Defaults to false
|
14
|
+
#
|
15
|
+
def initialize(host = 'localhost', port = 9595, headless = false)
|
16
|
+
@host, @port, @headless = host, port, headless
|
17
|
+
@pid = NullObject.new
|
18
|
+
setup_traps
|
19
|
+
end
|
20
|
+
|
21
|
+
# Queries if VLC is running
|
22
|
+
#
|
23
|
+
# @return [Boolean] true is VLC is running, false otherwise
|
24
|
+
#
|
25
|
+
def running?
|
26
|
+
not @pid.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
alias :started? :running?
|
30
|
+
|
31
|
+
# Queries if VLC is stopped
|
32
|
+
#
|
33
|
+
# @return [Boolean] true is VLC is stopped, false otherwise
|
34
|
+
#
|
35
|
+
def stopped?; not running?; end
|
36
|
+
|
37
|
+
# Starts a VLC instance in a subprocess
|
38
|
+
#
|
39
|
+
# @return [Integer] the subprocess PID or nil if the start command
|
40
|
+
# as no effect (e.g. VLC already running)
|
41
|
+
#
|
42
|
+
def start
|
43
|
+
return NullObject.new if running?
|
44
|
+
@pid = Process.fork do
|
45
|
+
STDIN.reopen "/dev/null"
|
46
|
+
STDOUT.reopen "/dev/null", "a"
|
47
|
+
STDERR.reopen "/dev/null", "a"
|
48
|
+
|
49
|
+
exec "#{headless? ? 'cvlc' : 'vlc'} --extraintf rc --rc-host #{@host}:#{@port}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Starts a VLC instance in a subprocess
|
54
|
+
#
|
55
|
+
# @return [Integer] the terminated subprocess PID or nil if the stop command
|
56
|
+
# as no effect (e.g. VLC not running)
|
57
|
+
#
|
58
|
+
def stop
|
59
|
+
return NullObject.new if not running?
|
60
|
+
|
61
|
+
Process.kill('INT', pid = @pid)
|
62
|
+
@pid = NullObject.new
|
63
|
+
pid
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def setup_traps
|
68
|
+
trap("EXIT") { stop }
|
69
|
+
trap("INT") { stop }
|
70
|
+
trap("CLD") { stop }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module VLC
|
2
|
+
# Local Client/Server VLC system
|
3
|
+
class System
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
# Creates a local VLC Client/Server system
|
7
|
+
#
|
8
|
+
# @overload initialize(host, port, options) sets the host and port
|
9
|
+
#
|
10
|
+
# @param [String] host The ip to connect to
|
11
|
+
# @param [Integer] port the port
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Boolean] :auto_start When false, the server lifecycle is not managed automatically and controll is passed to the developer
|
14
|
+
# @option options [Integer] :conn_retries Number of connection retries (each separated by a second) to make on auto-connect. Defaults to 5.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# vlc = VLC::System.new('10.10.0.10', 9000)
|
18
|
+
#
|
19
|
+
# @overload initialize()
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# vlc = VLC::System.new
|
23
|
+
#
|
24
|
+
# @return [VLC::Server]
|
25
|
+
#
|
26
|
+
# @raise [VLC::ConnectionRefused] if the connection fails
|
27
|
+
#
|
28
|
+
def initialize(*args)
|
29
|
+
args = NullObject.Null?(args)
|
30
|
+
opts = args.extract_options!
|
31
|
+
|
32
|
+
server = VLC::Server.new
|
33
|
+
server.headless = opts.fetch(:headless, false)
|
34
|
+
|
35
|
+
if args.size == 2
|
36
|
+
server.host = args.first.to_s
|
37
|
+
server.port = Integer(args.last)
|
38
|
+
end
|
39
|
+
@client = VLC::Client.new(server, opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def server
|
43
|
+
client.server
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(method, private_methods = false)
|
47
|
+
client.respond_to?(method, private_methods) || super(method, private_methods)
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
# Delegate to VLC::Client
|
52
|
+
#
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
return super unless client.respond_to?(method)
|
55
|
+
client.send(method, *args, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
#setup simplecov
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter "/spec"
|
7
|
+
end
|
8
|
+
|
9
|
+
require File.expand_path('../../lib/vlc-client', __FILE__)
|
10
|
+
|
11
|
+
module Mocks
|
12
|
+
def mock_tcp_server(opts = {})
|
13
|
+
tcp = double()
|
14
|
+
TCPSocket.stub(:new).and_return do
|
15
|
+
if opts.fetch(:defaults, true)
|
16
|
+
tcp.should_receive(:gets).with(no_args).at_least(:twice).and_return("")
|
17
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
18
|
+
tcp.should_receive(:close).with(no_args) if opts.fetch(:close, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
yield(tcp) if block_given?
|
22
|
+
tcp
|
23
|
+
end
|
24
|
+
tcp
|
25
|
+
end
|
26
|
+
|
27
|
+
def mock_system_calls(opts = {})
|
28
|
+
Process.stub(:fork).and_return(1)
|
29
|
+
Process.should_receive(:kill).once.with('INT', 1) if opts.fetch(:kill, true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def mock_sub_systems
|
33
|
+
mock_system_calls
|
34
|
+
mock_tcp_server
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.configure do |cfg|
|
39
|
+
cfg.include(Mocks)
|
40
|
+
end
|
data/spec/system_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
describe VLC::System do
|
2
|
+
before(:each) do
|
3
|
+
mock_system_calls(:kill => false)
|
4
|
+
end
|
5
|
+
|
6
|
+
subject { VLC::System.new }
|
7
|
+
|
8
|
+
it 'creates a self-managed VLC media client/server local system' do
|
9
|
+
mock_tcp_server(:close => false)
|
10
|
+
|
11
|
+
subject.client.should be_a(VLC::Client)
|
12
|
+
subject.server.should be_a(VLC::Server)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'delegates calls to the client' do
|
16
|
+
mock_tcp_server(:close => false).should_receive(:puts).with('is_playing').once
|
17
|
+
|
18
|
+
should respond_to(:play)
|
19
|
+
should_not be_playing
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'handles server lifecycle management to client code' do
|
23
|
+
mock_tcp_server(:close => false)
|
24
|
+
|
25
|
+
vlc = VLC::System.new('127.0.0.1', 9999, :auto_start => false)
|
26
|
+
|
27
|
+
vlc.server.should_not be_running
|
28
|
+
vlc.server.host.should eq('127.0.0.1')
|
29
|
+
vlc.server.port.should eq(9999)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
describe VLC::Client::ConnectionManagement do
|
2
|
+
let(:vlc) { VLC::Client.new(:self_managed => false) }
|
3
|
+
before(:each) { mock_tcp_server }
|
4
|
+
after(:each) { vlc.disconnect }
|
5
|
+
|
6
|
+
context 'when disconnected' do
|
7
|
+
specify { vlc.should be_disconnected }
|
8
|
+
|
9
|
+
it 'connects to VLC' do
|
10
|
+
vlc.connect
|
11
|
+
vlc.should be_connected
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when connected' do
|
16
|
+
before(:each) { vlc.connect }
|
17
|
+
specify { vlc.should be_connected }
|
18
|
+
|
19
|
+
it 'disconnects to VLC' do
|
20
|
+
vlc.disconnect
|
21
|
+
vlc.should be_disconnected
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
describe VLC::Client::MediaControls do
|
2
|
+
let(:vlc) { VLC::Client.new(:self_managed => false) }
|
3
|
+
after(:each) { vlc.disconnect }
|
4
|
+
|
5
|
+
context 'plays media' do
|
6
|
+
it 'from filesystem' do
|
7
|
+
mock_tcp_server.should_receive(:puts).once.with('add ./media.mp3')
|
8
|
+
vlc.connect
|
9
|
+
|
10
|
+
vlc.play('./media.mp3')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'from a file descriptor' do
|
14
|
+
File.stub(:open).and_return {
|
15
|
+
f = File.new('./LICENSE', 'r')
|
16
|
+
f.should_receive(:path).once.and_return('./media.mp3')
|
17
|
+
f
|
18
|
+
}
|
19
|
+
|
20
|
+
mock_tcp_server.should_receive(:puts).once.with('add ./media.mp3')
|
21
|
+
vlc.connect
|
22
|
+
|
23
|
+
vlc.play(File.open("./media.mp3"))
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'from web' do
|
27
|
+
mock_tcp_server.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
28
|
+
vlc.connect
|
29
|
+
|
30
|
+
vlc.play('http://example.org/media.mp3')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises error for unknown schemas' do
|
34
|
+
mock_tcp_server
|
35
|
+
vlc.connect
|
36
|
+
|
37
|
+
expect { vlc.play(Class.new) }.to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when playing media' do
|
42
|
+
def tcp_mock
|
43
|
+
tcp = mock_tcp_server(:defaults => false)
|
44
|
+
|
45
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
46
|
+
tcp.should_receive(:gets).with(no_args).twice.and_return("")
|
47
|
+
|
48
|
+
tcp.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
49
|
+
|
50
|
+
tcp.should_receive(:close).with(no_args)
|
51
|
+
tcp
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'may stop playback' do
|
55
|
+
tcp = tcp_mock
|
56
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
57
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("0")
|
58
|
+
|
59
|
+
tcp.should_receive(:puts).once.with('play')
|
60
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
61
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("1")
|
62
|
+
|
63
|
+
tcp.should_receive(:puts).once.with("stop")
|
64
|
+
|
65
|
+
vlc.connect
|
66
|
+
vlc.play('http://example.org/media.mp3')
|
67
|
+
|
68
|
+
vlc.stop
|
69
|
+
vlc.should be_stopped
|
70
|
+
|
71
|
+
vlc.play #play current item
|
72
|
+
vlc.should be_playing
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'may pause playback' do
|
76
|
+
tcp = tcp_mock
|
77
|
+
|
78
|
+
tcp.should_receive(:puts).once.with('pause')
|
79
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
80
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("0")
|
81
|
+
|
82
|
+
|
83
|
+
vlc.connect
|
84
|
+
vlc.play('http://example.org/media.mp3')
|
85
|
+
|
86
|
+
vlc.pause
|
87
|
+
vlc.should be_stopped
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'may resume playback' do
|
91
|
+
tcp = tcp_mock
|
92
|
+
|
93
|
+
tcp.should_receive(:puts).once.with('pause')
|
94
|
+
tcp.should_receive(:puts).once.with('play')
|
95
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
96
|
+
tcp.should_receive(:gets).once.with(no_args).and_return('1')
|
97
|
+
|
98
|
+
vlc.connect
|
99
|
+
vlc.play('http://example.org/media.mp3')
|
100
|
+
|
101
|
+
vlc.pause
|
102
|
+
vlc.play
|
103
|
+
vlc.should be_playing
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'displays the playing media title' do
|
107
|
+
tcp = tcp_mock
|
108
|
+
tcp.should_receive(:puts).once.with('get_title')
|
109
|
+
tcp.should_receive(:gets).once.and_return('test media')
|
110
|
+
tcp.should_receive(:puts).once.with('stop')
|
111
|
+
tcp.should_receive(:puts).once.with('get_title')
|
112
|
+
tcp.should_receive(:gets).once.and_return('')
|
113
|
+
|
114
|
+
vlc.connect
|
115
|
+
vlc.play('http://example.org/media.mp3')
|
116
|
+
|
117
|
+
vlc.title.should eq('test media')
|
118
|
+
vlc.stop
|
119
|
+
|
120
|
+
vlc.title.should be_empty
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'is aware of track time' do
|
124
|
+
tcp = tcp_mock
|
125
|
+
|
126
|
+
tcp.should_receive(:puts).once.with('get_time')
|
127
|
+
tcp.should_receive(:gets).once.and_return('60')
|
128
|
+
|
129
|
+
vlc.connect
|
130
|
+
vlc.play('http://example.org/media.mp3')
|
131
|
+
|
132
|
+
vlc.time.should eq(60)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'is aware of track length' do
|
136
|
+
tcp = tcp_mock
|
137
|
+
|
138
|
+
tcp.should_receive(:puts).once.with('get_length')
|
139
|
+
tcp.should_receive(:gets).once.and_return('100')
|
140
|
+
|
141
|
+
vlc.connect
|
142
|
+
vlc.play('http://example.org/media.mp3')
|
143
|
+
|
144
|
+
vlc.length.should eq(100)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'is aware of track progress' do
|
148
|
+
vlc.stub(:length).and_return { 0 }
|
149
|
+
vlc.stub(:time).and_return { 100 }
|
150
|
+
vlc.progress.should eq(0)
|
151
|
+
|
152
|
+
vlc.stub(:length).and_return { 100 }
|
153
|
+
vlc.stub(:time).and_return { 0 }
|
154
|
+
vlc.progress.should eq(0)
|
155
|
+
|
156
|
+
vlc.stub(:length).and_return { 100 }
|
157
|
+
vlc.stub(:time).and_return { 10 }
|
158
|
+
vlc.progress.should eq(10)
|
159
|
+
|
160
|
+
vlc.stub(:length).and_return { 100 }
|
161
|
+
vlc.stub(:time).and_return { 100 }
|
162
|
+
vlc.progress.should eq(100)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'is aware of current status' do
|
167
|
+
tcp = mock_tcp_server(:defaults => false)
|
168
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
169
|
+
|
170
|
+
tcp.should_receive(:gets).with(no_args).twice.and_return("")
|
171
|
+
|
172
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
173
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("> > 0")
|
174
|
+
|
175
|
+
tcp.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
176
|
+
|
177
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
178
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("> > 1")
|
179
|
+
|
180
|
+
tcp.should_receive(:close).with(no_args)
|
181
|
+
|
182
|
+
vlc.connect
|
183
|
+
|
184
|
+
vlc.should be_stopped
|
185
|
+
vlc.play('http://example.org/media.mp3')
|
186
|
+
vlc.should be_playing
|
187
|
+
end
|
188
|
+
end
|