sleeproom 0.4.0 → 0.9.0.beta3
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/.gitignore +2 -0
- data/Gemfile +3 -2
- data/README.md +0 -1
- data/Rakefile +3 -1
- data/bin/sleeproom +1 -2
- data/lib/sleeproom/async/websocket.rb +8 -6
- data/lib/sleeproom/cli.rb +32 -20
- data/lib/sleeproom/record.rb +8 -6
- data/lib/sleeproom/record/api/api.rb +3 -7
- data/lib/sleeproom/record/api/room.rb +2 -1
- data/lib/sleeproom/record/api/room_api.rb +2 -1
- data/lib/sleeproom/record/api/streaming_api.rb +4 -3
- data/lib/sleeproom/record/plugins.rb +9 -0
- data/lib/sleeproom/record/plugins/youtube/upload.rb +1 -0
- data/lib/sleeproom/record/record.rb +165 -193
- data/lib/sleeproom/record/tasks.rb +63 -56
- data/lib/sleeproom/record/websocket.rb +75 -69
- data/lib/sleeproom/record/write_status.rb +32 -2
- data/lib/sleeproom/utils.rb +72 -24
- data/lib/sleeproom/version.rb +1 -1
- data/sleeproom.gemspec +11 -8
- metadata +65 -22
- data/Gemfile.lock +0 -126
@@ -1,69 +1,63 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require "terminal-table"
|
3
4
|
|
4
5
|
module SleepRoom
|
5
6
|
module Record
|
6
7
|
class Tasks
|
7
8
|
# @return [void]
|
8
|
-
def self.start
|
9
|
-
Async do |
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
SleepRoom.create_pid(Process.pid)
|
20
|
-
lists = SleepRoom.load_config(:record)
|
21
|
-
lists.each do |group, list|
|
22
|
-
SleepRoom.info("Empty list.") if list.empty?
|
23
|
-
list.each do |room|
|
24
|
-
record = SleepRoom::Record::Showroom.new(room: room["room"], group: group, queue: write_status)
|
25
|
-
record.record
|
26
|
-
count += 1
|
9
|
+
def self.start(verbose: false)
|
10
|
+
Async do |task|
|
11
|
+
begin
|
12
|
+
running_task_count = 0
|
13
|
+
write_status = WriteStatus.new
|
14
|
+
write_status.run
|
15
|
+
if SleepRoom.running?
|
16
|
+
SleepRoom.error("PID #{SleepRoom.load_pid} Process is already running.")
|
17
|
+
exit
|
18
|
+
else
|
19
|
+
SleepRoom.write_config_file(:status, [])
|
27
20
|
end
|
28
|
-
|
29
|
-
SleepRoom.
|
21
|
+
SleepRoom.create_pid(Process.pid)
|
22
|
+
lists = SleepRoom.load_config(:record)
|
23
|
+
lists.each do |group, list|
|
24
|
+
begin
|
25
|
+
SleepRoom.info("Empty list.") if list.empty?
|
26
|
+
list.each do |room|
|
27
|
+
Async do
|
28
|
+
running_task_count += 1
|
29
|
+
record = SleepRoom::Record::Showroom.new(room: room["room"], group: group, queue: write_status)
|
30
|
+
record.record
|
31
|
+
end
|
32
|
+
end
|
33
|
+
rescue StandardError
|
34
|
+
SleepRoom.error("Cannot parse Recording list.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
SleepRoom.info("共启动 #{running_task_count} 个任务.")
|
38
|
+
task.children.each(&:wait)
|
39
|
+
rescue StandardError => e
|
40
|
+
puts e.full_message
|
30
41
|
end
|
31
|
-
write_status.run
|
32
|
-
SleepRoom.info("共启动 #{count} 个任务.")
|
33
|
-
wait
|
34
|
-
rescue => e
|
35
|
-
puts e.full_message
|
36
42
|
end
|
37
|
-
rescue Exception
|
43
|
+
rescue Exception => e
|
44
|
+
puts e.full_message if verbose
|
38
45
|
SleepRoom.create_pid(nil) unless SleepRoom.running?
|
39
46
|
puts "Exit..."
|
40
47
|
end
|
41
48
|
|
42
49
|
# @return [void]
|
43
50
|
def self.stop
|
44
|
-
SleepRoom.reload_config
|
45
51
|
raise "未实现"
|
46
52
|
end
|
47
53
|
|
48
54
|
# @return [void]
|
49
55
|
def self.status
|
50
56
|
Async do
|
51
|
-
SleepRoom.
|
52
|
-
status = SleepRoom.load_status
|
57
|
+
status = SleepRoom.load_status.sort_by { |hash| hash[:group] }
|
53
58
|
pid = SleepRoom.load_config(:pid)
|
54
59
|
if !SleepRoom.running?(pid) || status.empty? || pid.nil?
|
55
|
-
lists = SleepRoom.load_config(:record)
|
56
60
|
SleepRoom.info("No tasks running.")
|
57
|
-
lists.each do |group, list|
|
58
|
-
next if list.empty?
|
59
|
-
rows = []
|
60
|
-
title = group
|
61
|
-
headings = list[0].keys
|
62
|
-
list.each do |hash|
|
63
|
-
rows.push(hash.values)
|
64
|
-
end
|
65
|
-
puts Terminal::Table.new(title: "[Recording list] Group: #{title}",:rows => rows, headings: headings)
|
66
|
-
end
|
67
61
|
else
|
68
62
|
rows = []
|
69
63
|
headings = status[0].keys
|
@@ -71,7 +65,7 @@ module SleepRoom
|
|
71
65
|
rows.push(
|
72
66
|
hash.values.map do |s|
|
73
67
|
if s.is_a?(Hash)
|
74
|
-
|
68
|
+
((s[:last_ack].is_a?(Time) ? "[ACK]" + s[:last_ack].strftime("%H:%M:%S").to_s : "nil")).to_s
|
75
69
|
elsif s.is_a?(Time)
|
76
70
|
s.strftime("%H:%M:%S")
|
77
71
|
else
|
@@ -80,18 +74,40 @@ module SleepRoom
|
|
80
74
|
end
|
81
75
|
)
|
82
76
|
end
|
83
|
-
puts Terminal::Table.new(title: "Status [PID #{pid}] (#{status.count})"
|
77
|
+
puts Terminal::Table.new(title: "Status [PID #{pid}] (#{status.count})", rows: rows, headings: headings)
|
84
78
|
end
|
85
79
|
end
|
86
80
|
end
|
87
81
|
|
82
|
+
def self.lists
|
83
|
+
lists = SleepRoom.load_config(:record)
|
84
|
+
puts "[Record list]"
|
85
|
+
lists.each do |group, list|
|
86
|
+
next if list.empty?
|
87
|
+
|
88
|
+
rows = []
|
89
|
+
title = group
|
90
|
+
headings = list[0].keys
|
91
|
+
list.each do |hash|
|
92
|
+
rows.push(hash.values)
|
93
|
+
end
|
94
|
+
puts Terminal::Table.new(title: title, rows: rows, headings: headings)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
88
98
|
def self.add(room, group)
|
89
99
|
Async do
|
90
|
-
|
100
|
+
if group.empty?
|
101
|
+
if group_match = room.match(/(?<=[((]).*?(?=[))])/)
|
102
|
+
group = group_match[0]
|
103
|
+
else
|
104
|
+
group = "default"
|
105
|
+
end
|
106
|
+
end
|
91
107
|
old_record = SleepRoom.load_config(:record)
|
92
108
|
name = API::RoomAPI.new(room).room_name
|
93
|
-
input_record = {"room" => room, "name" => name}
|
94
|
-
if !old_record[group].nil? &&
|
109
|
+
input_record = { "room" => room, "name" => name }
|
110
|
+
if !old_record[group].nil? && old_record[group].find { |h| h = input_record if h["room"] == room }
|
95
111
|
SleepRoom.error("Room #{room} already exists.")
|
96
112
|
else
|
97
113
|
old_record[group] = [] if old_record[group].nil?
|
@@ -105,19 +121,10 @@ module SleepRoom
|
|
105
121
|
|
106
122
|
def self.remove(room)
|
107
123
|
old_record = SleepRoom.load_config(:record)
|
108
|
-
new_record = old_record.each {|
|
124
|
+
new_record = old_record.each { |_k, v| v.delete_if { |h| h["room"] == room } }
|
109
125
|
SleepRoom.write_config_file(:record, new_record)
|
110
126
|
SleepRoom.info("Remove success.")
|
111
127
|
end
|
112
|
-
|
113
|
-
private
|
114
|
-
def self.wait
|
115
|
-
Async do |task|
|
116
|
-
while true
|
117
|
-
task.sleep 1
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
128
|
end
|
122
129
|
end
|
123
130
|
end
|
@@ -1,86 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "sleeproom/async/websocket"
|
2
4
|
require "async/http/endpoint"
|
3
5
|
require "async/websocket/client"
|
4
6
|
require "json"
|
5
7
|
|
6
8
|
module SleepRoom
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
9
|
+
module Record
|
10
|
+
class WebSocket
|
11
|
+
def initialize(room:, broadcast_key:, url:, open_handler:, message_handler:, close_handler:, error_handler:, ping_handler:)
|
12
|
+
@room = room
|
13
|
+
@url = "wss://" + url
|
14
|
+
@broadcast_key = broadcast_key
|
15
|
+
@running = false
|
16
|
+
@endpoint = Async::HTTP::Endpoint.parse(@url)
|
17
|
+
@open_handler = open_handler
|
18
|
+
@message_handler = message_handler
|
19
|
+
@close_handler = close_handler
|
20
|
+
@error_handler = error_handler
|
21
|
+
@ping_handler = ping_handler
|
22
|
+
end
|
17
23
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
@running = true
|
24
|
-
connection.write("SUB\t#{@broadcast_key}")
|
25
|
-
connection.flush
|
26
|
-
log("Connect to websocket server.")
|
27
|
-
@queue.enqueue({event: :connect, time: Time.now})
|
28
|
-
|
29
|
-
ping_task = task.async do |sub|
|
30
|
-
while @running
|
31
|
-
sub.sleep 60
|
32
|
-
@queue.enqueue({event: :ping, time: Time.now})
|
33
|
-
connection.write("PING\tshowroom")
|
34
|
-
connection.flush
|
35
|
-
end
|
36
|
-
end
|
24
|
+
def connect(task: Async::Task.current)
|
25
|
+
websocket = Async::WebSocket::Client.connect(@endpoint, handler: WebSocketConnection) do |connection|
|
26
|
+
@connection = connection
|
27
|
+
@running = true
|
28
|
+
send("SUB\t#{@broadcast_key}")
|
37
29
|
|
38
|
-
|
39
|
-
|
40
|
-
sub.sleep 1
|
41
|
-
if @running == false
|
42
|
-
connection.close
|
43
|
-
end
|
44
|
-
end
|
45
|
-
end
|
30
|
+
log("Connect to websocket server.")
|
31
|
+
@open_handler.call
|
46
32
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
begin
|
53
|
-
yield JSON.parse(message.split("\t")[2])
|
54
|
-
rescue => e
|
55
|
-
SleepRoom.error(e.message)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
59
|
-
rescue => e
|
60
|
-
SleepRoom.error(e.message)
|
61
|
-
ensure
|
62
|
-
ping_task&.stop
|
63
|
-
connection.close
|
64
|
-
log("WebSocket closed.")
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def running?
|
69
|
-
@running
|
33
|
+
ping = task.async do |task|
|
34
|
+
loop do
|
35
|
+
task.sleep 60
|
36
|
+
send("PING\tshowroom")
|
37
|
+
end
|
70
38
|
end
|
71
39
|
|
72
|
-
|
73
|
-
|
74
|
-
|
40
|
+
while message = connection.read
|
41
|
+
debug("ACK: #{message}")
|
42
|
+
@ping_handler.call if message == "ACK\tshowroom"
|
75
43
|
|
76
|
-
|
77
|
-
@running = false
|
78
|
-
@connection.close
|
79
|
-
end
|
44
|
+
next unless message.start_with?("MSG")
|
80
45
|
|
81
|
-
|
82
|
-
|
46
|
+
begin
|
47
|
+
@message_handler.call(JSON.parse(message.split("\t")[2]))
|
48
|
+
rescue JSON::ParserError => e
|
49
|
+
@error_handler.call(e)
|
50
|
+
log(e.message)
|
51
|
+
end
|
83
52
|
end
|
53
|
+
rescue => e
|
54
|
+
error "error"
|
55
|
+
@error_handler.call(e)
|
56
|
+
log(e.full_message)
|
57
|
+
ensure
|
58
|
+
ping&.stop
|
59
|
+
@close_handler.call(nil)
|
60
|
+
@running = false
|
61
|
+
log("WebSocket closed.")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def send(data)
|
66
|
+
debug("SEND: #{data}")
|
67
|
+
@connection.write(data)
|
68
|
+
@connection.flush
|
69
|
+
end
|
70
|
+
|
71
|
+
def running?
|
72
|
+
@running
|
73
|
+
end
|
74
|
+
|
75
|
+
def stop
|
76
|
+
@connection.close
|
77
|
+
end
|
78
|
+
|
79
|
+
def log(str)
|
80
|
+
SleepRoom.info("[#{@room}] #{str}")
|
81
|
+
end
|
82
|
+
|
83
|
+
def error(str)
|
84
|
+
SleepRoom.error("[#{@room}] #{str}")
|
85
|
+
end
|
86
|
+
|
87
|
+
def debug(str)
|
88
|
+
SleepRoom.debug("[#{@room}] #{str}")
|
84
89
|
end
|
85
90
|
end
|
91
|
+
end
|
86
92
|
end
|
@@ -15,9 +15,11 @@ module SleepRoom
|
|
15
15
|
status[:update] = Time.now
|
16
16
|
old_status = SleepRoom.load_config(:status)
|
17
17
|
room = status[:room]
|
18
|
-
if old_status.find { |h| h[:room] == room }
|
18
|
+
if tmp_status = old_status.find { |h| h[:room] == room }
|
19
19
|
new_status = old_status.delete_if { |h| h[:room] == room }
|
20
|
-
|
20
|
+
unless tmp_status[:status] == :downloading && status[:status] == :waiting
|
21
|
+
new_status.push(tmp_status.merge!(status))
|
22
|
+
end
|
21
23
|
else
|
22
24
|
new_status = old_status.push(status)
|
23
25
|
end
|
@@ -31,6 +33,34 @@ module SleepRoom
|
|
31
33
|
@queue.enqueue(status)
|
32
34
|
end
|
33
35
|
end
|
36
|
+
|
37
|
+
def downloading(room:, url:, pid:, start_time:, output:)
|
38
|
+
add(
|
39
|
+
{
|
40
|
+
room: room,
|
41
|
+
live: true,
|
42
|
+
status: :downloading,
|
43
|
+
streaming_url: url,
|
44
|
+
pid: pid,
|
45
|
+
start_time: start_time
|
46
|
+
}
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def waiting(room:, group:, room_name:, key:)
|
51
|
+
add(
|
52
|
+
{
|
53
|
+
room: room,
|
54
|
+
live: false,
|
55
|
+
group: group,
|
56
|
+
name: room_name,
|
57
|
+
status: :waiting,
|
58
|
+
websocket: {
|
59
|
+
key: key
|
60
|
+
}
|
61
|
+
}
|
62
|
+
)
|
63
|
+
end
|
34
64
|
end
|
35
65
|
end
|
36
66
|
end
|
data/lib/sleeproom/utils.rb
CHANGED
@@ -3,11 +3,17 @@
|
|
3
3
|
require "configatron"
|
4
4
|
require "colorize"
|
5
5
|
require "fileutils"
|
6
|
+
require "tmpdir"
|
6
7
|
require "yaml"
|
7
8
|
require "logger"
|
9
|
+
require "dry/files"
|
8
10
|
|
9
11
|
module SleepRoom
|
10
12
|
class Error < StandardError; end
|
13
|
+
def self.files
|
14
|
+
Dry::Files.new
|
15
|
+
end
|
16
|
+
|
11
17
|
# @return [String]
|
12
18
|
def self.root_path
|
13
19
|
Dir.pwd
|
@@ -64,6 +70,7 @@ module SleepRoom
|
|
64
70
|
def self.create_config_file(config, settings)
|
65
71
|
path = config_path(config)
|
66
72
|
return false if File.exist?(path)
|
73
|
+
|
67
74
|
mkdir(File.dirname(path)) unless Dir.exist?(File.dirname(path))
|
68
75
|
write_config_file(config, settings)
|
69
76
|
end
|
@@ -83,15 +90,10 @@ module SleepRoom
|
|
83
90
|
|
84
91
|
def self.init_base
|
85
92
|
base = {
|
86
|
-
web: {
|
87
|
-
use: true,
|
88
|
-
server: "localhost",
|
89
|
-
port: 3000
|
90
|
-
},
|
91
93
|
proxy: {
|
92
94
|
use: false,
|
93
95
|
server: "localhost",
|
94
|
-
port:
|
96
|
+
port: 1080,
|
95
97
|
type: "socks5"
|
96
98
|
},
|
97
99
|
record: {
|
@@ -106,11 +108,12 @@ module SleepRoom
|
|
106
108
|
minyami: {
|
107
109
|
threads: 8,
|
108
110
|
retries: 999,
|
111
|
+
no_merge: false,
|
109
112
|
},
|
110
113
|
logger: {
|
111
114
|
console: true,
|
112
115
|
file: {
|
113
|
-
use:
|
116
|
+
use: false,
|
114
117
|
path: "#{sleeproom_dir}/log"
|
115
118
|
}
|
116
119
|
}
|
@@ -122,9 +125,9 @@ module SleepRoom
|
|
122
125
|
mkdir(config_dir) unless Dir.exist?(config_dir)
|
123
126
|
|
124
127
|
mkdir("#{config_dir}/tmp") unless Dir.exist?("#{config_dir}/tmp")
|
125
|
-
|
128
|
+
|
126
129
|
init_base
|
127
|
-
|
130
|
+
|
128
131
|
record = {
|
129
132
|
"default" => []
|
130
133
|
}
|
@@ -142,13 +145,15 @@ module SleepRoom
|
|
142
145
|
end
|
143
146
|
true
|
144
147
|
rescue Errno::ENOENT => e
|
145
|
-
|
148
|
+
info("Creating configuration...")
|
149
|
+
init_base
|
146
150
|
false
|
147
151
|
end
|
148
152
|
|
149
153
|
def self.settings
|
150
154
|
configatron
|
151
155
|
end
|
156
|
+
|
152
157
|
def self.load_status
|
153
158
|
SleepRoom.load_config(:status)
|
154
159
|
rescue Error
|
@@ -163,11 +168,11 @@ module SleepRoom
|
|
163
168
|
retry
|
164
169
|
end
|
165
170
|
|
166
|
-
def self.running?(pid=nil)
|
171
|
+
def self.running?(pid = nil)
|
167
172
|
pid = SleepRoom.load_config(:pid) if pid.nil?
|
168
|
-
Process.
|
173
|
+
Process.kill(0, pid)
|
169
174
|
true
|
170
|
-
rescue
|
175
|
+
rescue StandardError
|
171
176
|
false
|
172
177
|
end
|
173
178
|
|
@@ -182,7 +187,7 @@ module SleepRoom
|
|
182
187
|
SleepRoom.create_config_file(:status, status)
|
183
188
|
end
|
184
189
|
|
185
|
-
def self.create_record(record = {default: []})
|
190
|
+
def self.create_record(record = { default: [] })
|
186
191
|
SleepRoom.create_config_file(:record, record)
|
187
192
|
end
|
188
193
|
|
@@ -191,38 +196,81 @@ module SleepRoom
|
|
191
196
|
SleepRoom.write_config_file(:pid, pid)
|
192
197
|
end
|
193
198
|
|
199
|
+
def self.plugins
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.find_tmp_directory(output, call_time)
|
204
|
+
regex = /Proccessing (.*) finished./
|
205
|
+
output = "#{configatron.save_path}/#{output}"
|
206
|
+
log = "#{output}.out"
|
207
|
+
if media_name = File.readlines(log).select { |line| line =~ regex }.last.match(regex)[1]
|
208
|
+
directories = Dir["#{Dir.tmpdir}/minyami_#{call_time.to_i / 10}*"]
|
209
|
+
directories.each do |path|
|
210
|
+
if Dir.glob("#{path}/*.ts").select{ |e| e.include?(media_name)}
|
211
|
+
next unless Dir.glob("#{path}/*.ts").last.include?(media_name)
|
212
|
+
|
213
|
+
return path
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
return false
|
218
|
+
rescue => e
|
219
|
+
puts e.full_message
|
220
|
+
SleepRoom.error("寻找失败.")
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.move_ts_to_archive(tmp, path, name)
|
224
|
+
FileUtils.cp_r(tmp, path)
|
225
|
+
FileUtils.mv(File.join(path, File.basename(tmp)), File.join(path, name))
|
226
|
+
rescue => e
|
227
|
+
puts e.full_message
|
228
|
+
SleepRoom.error("复制失败.")
|
229
|
+
end
|
230
|
+
|
194
231
|
# @param string [String]
|
195
232
|
# @return [nil]
|
196
233
|
def self.info(string)
|
197
|
-
log(:info,
|
234
|
+
log(:info, string)
|
235
|
+
end
|
236
|
+
|
237
|
+
# @param string [String]
|
238
|
+
# @return [nil]
|
239
|
+
def self.debug(string)
|
240
|
+
log(:debug, string) if ENV["SR_DEBUG"]
|
198
241
|
end
|
199
242
|
|
200
243
|
# @param string [String]
|
201
244
|
# @return [nil]
|
202
245
|
def self.warning(string)
|
203
|
-
log(:warning,
|
246
|
+
log(:warning, string)
|
204
247
|
end
|
205
248
|
|
206
249
|
# @param string [String]
|
207
250
|
# @return [nil]
|
208
251
|
def self.error(string)
|
209
|
-
log(:error,
|
252
|
+
log(:error, string)
|
210
253
|
end
|
211
254
|
|
212
255
|
def self.log(type, log)
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
256
|
+
if configatron.logger.console == true
|
257
|
+
case type
|
258
|
+
when :info
|
259
|
+
puts("[INFO] #{log}".colorize(:white))
|
260
|
+
when :warning
|
261
|
+
warn("[WARN] #{log}".colorize(:yellow))
|
262
|
+
when :error
|
263
|
+
puts("[ERROR] #{log}".colorize(:red))
|
264
|
+
when :debug
|
265
|
+
puts("[DEBUG] #{log}".colorize(:gray))
|
266
|
+
end
|
220
267
|
end
|
221
268
|
file_logger(type, log) if configatron.logger.file.use == true
|
222
269
|
end
|
223
270
|
|
224
271
|
def self.file_logger(type, log)
|
225
272
|
path = configatron.logger.file.path
|
273
|
+
mkdir(File.dirname(path)) unless Dir.exist?(File.dirname(path))
|
226
274
|
logger = Logger.new(path)
|
227
275
|
case type
|
228
276
|
when :info
|