sleeproom 0.1.1
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 +7 -0
- data/.gitignore +15 -0
- data/.rspec +3 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +126 -0
- data/LICENSE.txt +21 -0
- data/README.md +40 -0
- data/Rakefile +6 -0
- data/bin/sleeproom +6 -0
- data/lib/sleeproom/async/websocket.rb +20 -0
- data/lib/sleeproom/cli.rb +78 -0
- data/lib/sleeproom/record.rb +49 -0
- data/lib/sleeproom/record/api/api.rb +31 -0
- data/lib/sleeproom/record/api/room.rb +17 -0
- data/lib/sleeproom/record/api/room_api.rb +39 -0
- data/lib/sleeproom/record/api/streaming_api.rb +23 -0
- data/lib/sleeproom/record/record.rb +210 -0
- data/lib/sleeproom/record/tasks.rb +123 -0
- data/lib/sleeproom/record/websocket.rb +91 -0
- data/lib/sleeproom/record/write_status.rb +36 -0
- data/lib/sleeproom/utils.rb +214 -0
- data/lib/sleeproom/version.rb +5 -0
- data/sleeproom.gemspec +41 -0
- metadata +157 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SleepRoom
|
4
|
+
module Record
|
5
|
+
module API
|
6
|
+
class RoomAPI
|
7
|
+
def initialize(room_url_key)
|
8
|
+
@url = ROOM_API + "?room_url_key=" + room_url_key
|
9
|
+
@json = nil
|
10
|
+
get
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(task: Async::Task.current)
|
14
|
+
@json = API.get(@url).wait
|
15
|
+
end
|
16
|
+
|
17
|
+
def live?
|
18
|
+
@json["is_live"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def broadcast_key
|
22
|
+
@json["broadcast_key"].to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
def broadcast_host
|
26
|
+
@json["broadcast_host"].to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def room_id
|
30
|
+
@json["room_id"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def room_name
|
34
|
+
@json["room_name"]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SleepRoom
|
4
|
+
module Record
|
5
|
+
module API
|
6
|
+
class StreamingAPI
|
7
|
+
def initialize(room_id)
|
8
|
+
@url = STREAMING_API + "?room_id=" + room_id.to_s + "&ignore_low_stream=1"
|
9
|
+
@json = nil
|
10
|
+
get
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(task: Async::Task.current)
|
14
|
+
@json = API.get(@url).wait
|
15
|
+
end
|
16
|
+
|
17
|
+
def streaming_url
|
18
|
+
@json["streaming_url_list"].sort_by{|hash| -hash["quality"]}.first["url"]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "sleeproom/record/write_status"
|
3
|
+
|
4
|
+
module SleepRoom
|
5
|
+
module Record
|
6
|
+
class Showroom
|
7
|
+
SITE = "showroom"
|
8
|
+
def initialize(room:, group: "default", queue:)
|
9
|
+
@room = room
|
10
|
+
@group = group
|
11
|
+
@queue = queue
|
12
|
+
@running = false
|
13
|
+
@downlaoding = false
|
14
|
+
@reconnection = false
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param user [String]
|
18
|
+
# @return [Boolean]
|
19
|
+
def record(reconnection: false)
|
20
|
+
room = @room
|
21
|
+
Async do |task|
|
22
|
+
api = API::RoomAPI.new(room)
|
23
|
+
task.async do |t|
|
24
|
+
while api.live?
|
25
|
+
if status = SleepRoom.load_config(:status).find{|hash| hash[:room] == room}
|
26
|
+
if !status[:pid].nil?
|
27
|
+
break if SleepRoom.running?(pid) == false
|
28
|
+
else
|
29
|
+
break
|
30
|
+
end
|
31
|
+
else
|
32
|
+
break
|
33
|
+
end
|
34
|
+
t.sleep 60
|
35
|
+
end
|
36
|
+
end.wait
|
37
|
+
@room_id = api.room_id
|
38
|
+
@room_name = api.room_name
|
39
|
+
@is_live = api.live?
|
40
|
+
@broadcast_host = api.broadcast_host
|
41
|
+
@broadcast_key = api.broadcast_key
|
42
|
+
if @is_live
|
43
|
+
start_time = Time.now
|
44
|
+
log("Live broadcast.")
|
45
|
+
streaming_url = parse_streaming_url
|
46
|
+
output = build_output
|
47
|
+
pid = SleepRoom::Record.call_minyami(url: streaming_url, output: output)
|
48
|
+
downloading(streaming_url, pid, start_time)
|
49
|
+
record
|
50
|
+
else
|
51
|
+
log("Status: Stop.")
|
52
|
+
waiting_live(ws: :init)
|
53
|
+
Async do |task|
|
54
|
+
while true
|
55
|
+
if @running == false && @reconnection == false
|
56
|
+
start_websocket
|
57
|
+
elsif @reconnection == true
|
58
|
+
api = API::RoomAPI.new(room)
|
59
|
+
@room_id = api.room_id
|
60
|
+
@room_name = api.room_name
|
61
|
+
@is_live = api.live?
|
62
|
+
@broadcast_host = api.broadcast_host
|
63
|
+
@broadcast_key = api.broadcast_key
|
64
|
+
start_websocket
|
65
|
+
@reconnection = false
|
66
|
+
end
|
67
|
+
task.sleep 10
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue => e
|
72
|
+
add_error(e)
|
73
|
+
SleepRoom.error(e.full_message)
|
74
|
+
log("Retry...")
|
75
|
+
task.sleep 5
|
76
|
+
retry
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def log(str)
|
81
|
+
SleepRoom.info("[#{@room}] #{str}")
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
def start_websocket()
|
86
|
+
Async do |task|
|
87
|
+
@running = true
|
88
|
+
log("Broadcast Key: #{@broadcast_key}")
|
89
|
+
waiting_live(ws: :init)
|
90
|
+
ws = WebSocket.new(room: @room, broadcast_key: @broadcast_key, url: @broadcast_host)
|
91
|
+
@ws = ws
|
92
|
+
# ws status
|
93
|
+
ws_task = task.async do |sub|
|
94
|
+
ws.running = true
|
95
|
+
ws.connect(task: sub) do |message|
|
96
|
+
case message["t"].to_i
|
97
|
+
when 101
|
98
|
+
log("Live stop.")
|
99
|
+
ws.running = false
|
100
|
+
@running = false
|
101
|
+
record
|
102
|
+
when 104
|
103
|
+
log("Live start.")
|
104
|
+
start_time = Time.now
|
105
|
+
streaming_url = parse_streaming_url
|
106
|
+
output = build_output
|
107
|
+
pid = SleepRoom::Record.call_minyami(url: streaming_url, output: output)
|
108
|
+
downloading(streaming_url, pid, start_time)
|
109
|
+
ws.running = false
|
110
|
+
@running = false
|
111
|
+
@reconnection = true
|
112
|
+
else
|
113
|
+
# other
|
114
|
+
end
|
115
|
+
end
|
116
|
+
rescue => e
|
117
|
+
SleepRoom.error("WS Stop.")
|
118
|
+
SleepRoom.error(e.full_message)
|
119
|
+
ws.running = false
|
120
|
+
@running = false
|
121
|
+
add_error(e)
|
122
|
+
end
|
123
|
+
|
124
|
+
Async do |task|
|
125
|
+
while @running && @downlaoding == false
|
126
|
+
status = ws.status
|
127
|
+
if !status[:last_ack].nil? && Time.now.to_i - status[:last_ack].to_i > 65
|
128
|
+
ws.running = false
|
129
|
+
@running = false
|
130
|
+
task.stop
|
131
|
+
end
|
132
|
+
waiting_live(status)
|
133
|
+
task.sleep 30
|
134
|
+
end
|
135
|
+
end
|
136
|
+
task.children.each(&:wait)
|
137
|
+
ensure
|
138
|
+
ws.running = false
|
139
|
+
@running = false
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def parse_streaming_url(task: Async::Task.current)
|
144
|
+
api = API::StreamingAPI.new(@room_id)
|
145
|
+
streaming_url_list = api.streaming_url
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_output(task: Async::Task.current)
|
149
|
+
room = @room
|
150
|
+
group = @group
|
151
|
+
tmp_str = configatron.default_save_name
|
152
|
+
tmp_str = tmp_str.sub("\%TIME\%", Time.now.strftime("%Y-%m-%d-%H-%M-%S")) if tmp_str.include?("\%TIME\%")
|
153
|
+
tmp_str = tmp_str.sub("\%ROOMNAME\%", room) if tmp_str.include?("\%ROOMNAME\%")
|
154
|
+
File.join(group, room, "showroom", tmp_str)
|
155
|
+
end
|
156
|
+
|
157
|
+
def downloading(streaming_url, pid, start_time, task: Async::Task.current)
|
158
|
+
@downlaoding = true
|
159
|
+
@queue.add({
|
160
|
+
room: @room,
|
161
|
+
start_time: start_time,
|
162
|
+
name: @room_name,
|
163
|
+
group: @group,
|
164
|
+
live: true,
|
165
|
+
status: :downloading,
|
166
|
+
streaming_url: streaming_url,
|
167
|
+
download_pid: pid
|
168
|
+
})
|
169
|
+
task.async do |t|
|
170
|
+
while true
|
171
|
+
if !SleepRoom.running?(pid) && !API::RoomAPI.new(@room).live?
|
172
|
+
log("Download complete.")
|
173
|
+
@downlaoding = false
|
174
|
+
@queue.add({
|
175
|
+
room: @room,
|
176
|
+
name: @room_name,
|
177
|
+
group: @group,
|
178
|
+
live: API::RoomAPI.new(@room).live?,
|
179
|
+
status: :complete,
|
180
|
+
})
|
181
|
+
break
|
182
|
+
end
|
183
|
+
t.sleep 60
|
184
|
+
end
|
185
|
+
end.wait
|
186
|
+
end
|
187
|
+
|
188
|
+
def add_error(error)
|
189
|
+
@queue.add({
|
190
|
+
room: @room,
|
191
|
+
name: @room_name,
|
192
|
+
group: @group,
|
193
|
+
status: :retry,
|
194
|
+
error: error.message
|
195
|
+
})
|
196
|
+
end
|
197
|
+
|
198
|
+
def waiting_live(status)
|
199
|
+
@queue.add({
|
200
|
+
room: @room,
|
201
|
+
live: false,
|
202
|
+
group: @group,
|
203
|
+
name: @room_name,
|
204
|
+
status: :waiting,
|
205
|
+
ws: status
|
206
|
+
})
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "terminal-table"
|
3
|
+
|
4
|
+
module SleepRoom
|
5
|
+
module Record
|
6
|
+
class Tasks
|
7
|
+
# @return [void]
|
8
|
+
def self.start
|
9
|
+
Async do |_task|
|
10
|
+
count = 0
|
11
|
+
write_status = WriteStatus.new
|
12
|
+
SleepRoom.reload_config
|
13
|
+
if SleepRoom.running?
|
14
|
+
SleepRoom.error("PID #{SleepRoom.load_pid} Process is already running.")
|
15
|
+
exit
|
16
|
+
else
|
17
|
+
SleepRoom.write_config_file(:status, [])
|
18
|
+
end
|
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
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
SleepRoom.error("Cannot parse Recording list.")
|
30
|
+
end
|
31
|
+
write_status.run
|
32
|
+
SleepRoom.info("共启动 #{count} 个任务.")
|
33
|
+
wait
|
34
|
+
rescue => e
|
35
|
+
puts e.full_message
|
36
|
+
end
|
37
|
+
rescue Exception
|
38
|
+
SleepRoom.create_pid(nil) unless SleepRoom.running?
|
39
|
+
puts "Exit..."
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [void]
|
43
|
+
def self.stop
|
44
|
+
SleepRoom.reload_config
|
45
|
+
raise "未实现"
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [void]
|
49
|
+
def self.status
|
50
|
+
Async do
|
51
|
+
SleepRoom.reload_config
|
52
|
+
status = SleepRoom.load_status
|
53
|
+
pid = SleepRoom.load_config(:pid)
|
54
|
+
if !SleepRoom.running?(pid) || status.empty? || pid.nil?
|
55
|
+
lists = SleepRoom.load_config(:record)
|
56
|
+
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
|
+
else
|
68
|
+
rows = []
|
69
|
+
headings = status[0].keys
|
70
|
+
status.each do |hash|
|
71
|
+
rows.push(
|
72
|
+
hash.values.map do |s|
|
73
|
+
if s.is_a?(Hash)
|
74
|
+
"#{(s[:last_ack].is_a?(Time) ? "[ACK]" + s[:last_ack].strftime("%H:%M:%S").to_s : "nil")}"
|
75
|
+
elsif s.is_a?(Time)
|
76
|
+
s.strftime("%H:%M:%S")
|
77
|
+
else
|
78
|
+
s.to_s
|
79
|
+
end
|
80
|
+
end
|
81
|
+
)
|
82
|
+
end
|
83
|
+
puts Terminal::Table.new(title: "Status [PID #{pid}] (#{status.count})",:rows => rows, headings: headings)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.add(room, group)
|
89
|
+
Async do
|
90
|
+
group = "default" if group.empty?
|
91
|
+
old_record = SleepRoom.load_config(:record)
|
92
|
+
name = API::RoomAPI.new(room).room_name
|
93
|
+
input_record = {"room" => room, "name" => name}
|
94
|
+
if !old_record[group].nil? && new_record = old_record[group].find{|h| h = input_record if h["room"] == room}
|
95
|
+
SleepRoom.error("Room #{room} already exists.")
|
96
|
+
else
|
97
|
+
old_record[group] = [] if old_record[group].nil?
|
98
|
+
old_record[group].push(input_record)
|
99
|
+
new_record = old_record
|
100
|
+
SleepRoom.write_config_file(:record, new_record)
|
101
|
+
SleepRoom.info("Added success.")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.remove(room)
|
107
|
+
old_record = SleepRoom.load_config(:record)
|
108
|
+
new_record = old_record.each {|k, v| v.delete_if { |h| h["room"] == room }}
|
109
|
+
SleepRoom.write_config_file(:record, new_record)
|
110
|
+
SleepRoom.info("Remove success.")
|
111
|
+
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
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require "sleeproom/async/websocket"
|
2
|
+
require "async/http/endpoint"
|
3
|
+
require "async/websocket/client"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module SleepRoom
|
7
|
+
module Record
|
8
|
+
class WebSocket
|
9
|
+
def initialize(room:, broadcast_key:, url:)
|
10
|
+
@room = room
|
11
|
+
@url = "wss://" + url
|
12
|
+
@broadcast_key = broadcast_key
|
13
|
+
@running = false
|
14
|
+
@status = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect(task: Async::Task.current)
|
18
|
+
url = @url
|
19
|
+
endpoint = Async::HTTP::Endpoint.parse(url)
|
20
|
+
Async::WebSocket::Client.connect(endpoint, handler: WebSocketConnection) do |connection|
|
21
|
+
@connection = connection
|
22
|
+
@running = true
|
23
|
+
connection.write("SUB\t#{@broadcast_key}")
|
24
|
+
connection.flush
|
25
|
+
log("Connect to websocket server.")
|
26
|
+
@status[:last_update] = Time.now
|
27
|
+
|
28
|
+
ping_task = task.async do |sub|
|
29
|
+
while @running
|
30
|
+
sub.sleep 60
|
31
|
+
@status[:last_ping] = Time.now
|
32
|
+
connection.write("PING\tshowroom")
|
33
|
+
connection.flush
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
status_task = task.async do |sub|
|
38
|
+
while true
|
39
|
+
sub.sleep 1
|
40
|
+
if @running == false
|
41
|
+
connection.close
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
while message = connection.read
|
47
|
+
@status[:last_update]
|
48
|
+
if message == "ACK\tshowroom"
|
49
|
+
@status[:last_ack] = Time.now if message == "ACK\tshowroom"
|
50
|
+
end
|
51
|
+
if message.start_with?("MSG")
|
52
|
+
@status[:last_msg] = Time.now
|
53
|
+
begin
|
54
|
+
yield JSON.parse(message.split("\t")[2])
|
55
|
+
rescue => e
|
56
|
+
SleepRoom.error(e.message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
rescue => e
|
61
|
+
SleepRoom.error(e.message)
|
62
|
+
ensure
|
63
|
+
ping_task&.stop
|
64
|
+
connection.close
|
65
|
+
log("WebSocket closed.")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def running?
|
70
|
+
@running
|
71
|
+
end
|
72
|
+
|
73
|
+
def running=(bool)
|
74
|
+
@running = bool
|
75
|
+
end
|
76
|
+
|
77
|
+
def stop
|
78
|
+
@running = false
|
79
|
+
@connection.close
|
80
|
+
end
|
81
|
+
|
82
|
+
def status
|
83
|
+
@status
|
84
|
+
end
|
85
|
+
|
86
|
+
def log(str)
|
87
|
+
SleepRoom.info("[#{@room}] #{str}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|