-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathserver.lua
252 lines (216 loc) · 9.11 KB
/
server.lua
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
--- Server program that actually plays audio.
local transmission = require "transmission"
local file_helper = require "file_helper"
local aukit = require "aukit"
local logging = require "logging"
local deep_copy = require "deep_copy"
local QIT = require "QIT"
local DIR = fs.getDir(shell.getRunningProgram())
local CONFIG_FILE = fs.combine(DIR, "server-config.lson")
local main_context = logging.createContext("MAIN", colors.black, colors.blue)
local mon = peripheral.find "monitor"
local log_win = window.create(term.current(), 1, 1, term.getSize())
logging.setWin(log_win)
local config = file_helper.unserialize(CONFIG_FILE, {
channel = 1470,
response_channel = 1471
})
if ... == "debug" then
logging.setLevel(logging.logLevel.DEBUG)
logging.setFile("fatmusic_debug-server.txt")
end
main_context.debug("Starting server in '/%s'", DIR)
main_context.debug("Config : /%s", CONFIG_FILE)
main_context.debug("Finding modem.")
local modem
modem = peripheral.find("modem", function(_, wrapped) return wrapped.isWireless() end)
if not modem then
modem = peripheral.find("modem")
end
if not modem then
main_context.error("No modem is connected to the computer!", 0)
return
end
local transmitter = transmission.create(config.channel, config.response_channel, modem)
--- Run the server.
local function server()
local playlist = QIT() ---@type QIT<song_info>
local currently_playing ---@type song_info?
--- Add music to the playlist.
---@param name string
---@param remote string
local function play_audio(name, remote)
playlist:Insert({ name = name, remote = remote }--[[@as song_info]] )
end
--- Get the current playlist as an action to send to the client.
---@return Arrayn<song_info> playlist The playlist information
local function get_playlist()
return deep_copy(playlist):Clean()
end
--- Get the currently playing song.
---@return song_info? currently_playing The currently playing song information.
local function get_currently_playing()
return deep_copy(currently_playing)
end
--- When the song updates, get information about everything and transmit it.
---@return action song_update The updated information.
local function get_broadcast_info()
return transmission.make_action(
"song-update",
{
playing = deep_copy(currently_playing),
playlist = deep_copy(playlist):Clean()
}
)
end
--- Get the next song in the queue.
---@return song_info song_info The song information
local function get_next_song()
return playlist:Drop()
end
local seen_messages = {}
parallel.waitForAny(
--- Listener coroutine - listen for commands from clients.
function()
local listener_context = logging.createContext("LISTENER", colors.black, colors.yellow)
listener_context.info("Listening for commands.")
while true do
local action = transmitter:receive()
if seen_messages[action.system_id] and seen_messages[action.system_id][action.session_id] and
seen_messages[action.system_id][action.session_id][action.transmission_id] then
-- If we receive a duplicate message, send the same response back.
transmitter:send(seen_messages[action.system_id][action.session_id][action.transmission_id], true)
listener_context.warn("Got duplicate message, resending response.")
else
local response
if not seen_messages[action.system_id] then
seen_messages[action.system_id] = {}
end
if not seen_messages[action.system_id][action.session_id] then
seen_messages[action.system_id][action.session_id] = {}
end
if action.action == "get-playlist" then
listener_context.debug("Get playlist")
response = transmission.ack(action, get_playlist())
elseif action.action == "get-playing" then
listener_context.debug("Get currently playing")
response = transmission.ack(action, get_currently_playing())
elseif action.action == "add-to-playlist" then
listener_context.debug("Add to playlist")
if type(action.data) == "table" then
if not action.data.name then
listener_context.error("Received song information table missing field 'name'.")
response = transmission.error(action, "Song information table is missing field 'name'.")
elseif not action.data.remote then
listener_context.error("Received song information table missing field 'remote'.")
response = transmission.error(action, "Song information table is missing field 'remote'.")
else
listener_context.info("Add to playlist: %s (%s)", action.data.name, action.data.remote)
play_audio(action.data.name, action.data.remote)
response = transmission.ack(action)
os.queueEvent("fatmusic:song_update") -- broadcast the new playlist.
end
else
listener_context.error("Received song information was not a table.")
response = transmission.error(action, "Expected song information table for data.")
end
elseif action.action == "stop" then
listener_context.debug("Stop")
playlist = QIT()
os.queueEvent("fatmusic:stop")
response = transmission.ack(action)
elseif action.action == "skip" then
listener_context.debug("Skip")
os.queueEvent("fatmusic:skip")
response = transmission.ack(action)
elseif action.action == "alive" then
response = transmission.ack(action)
end
seen_messages[action.system_id][action.transmission_id] = response
transmitter:send(response, true)
end
end
end,
--- Music update coroutine - Send information to clients when song changes.
function()
while true do
os.pullEvent("fatmusic:song_update")
transmitter:send(get_broadcast_info())
end
end,
--- Music playing coroutine - Plays the music.
function()
local play_context = logging.createContext("PLAYER", colors.black, colors.green)
local was_playing = false
while true do
currently_playing = get_next_song()
if currently_playing then
play_context.info("Next song: %s", currently_playing.name)
play_context.debug("Playing from: %s", currently_playing.remote)
os.queueEvent("fatmusic:song_update")
was_playing = true
parallel.waitForAny(
--- Actually plays the music, displays info to the monitor as well.
function()
if currently_playing.remote:match("%.wav$") then
play_context.debug("Downloading remote...")
mon.clear()
mon.setCursorPos(3, 2)
mon.write("Downloading...")
mon.setCursorPos(3, 4)
mon.write("--:-- / --:--")
local handle, err = http.get(currently_playing.remote, nil, true)
if not handle then
play_context.error("Failed to download file: %s", err)
return
end
local data = handle.readAll()
handle.close()
play_context.info("Song downloaded.")
local speakers = { peripheral.find "speaker" }
local iter, length = aukit.stream.wav(data, true)
local formatter = "%02d:%02d / %02d:%02d"
local w, h = mon.getSize()
mon.clear()
mon.setCursorPos(3, 2)
mon.write(currently_playing.name)
play_context.debug("Begin playing song.")
aukit.play(iter, function(pos)
pos = math.min(pos, 5999)
mon.setCursorPos(3, 4)
mon.write(formatter:format(math.floor(pos / 60), pos % 60, math.floor(length / 60), length % 60))
end, 1, table.unpack(speakers))
play_context.debug("Song finished.")
else
play_context.error("Song %s is not of .wav type.", currently_playing.name)
transmitter:send(transmission.make_action("song-error", nil,
("Song %s is not of .wav type."):format(currently_playing.name)), true)
end
end,
--- Listens for stop or skip events, stops the song that is currently playing when it receives one.
function()
while true do
local event = os.pullEvent()
if event == "fatmusic:stop" or event == "fatmusic:skip" then
play_context.warn("Stop or skip requested during playback.")
break
end
end
end
)
mon.clear()
elseif was_playing then
was_playing = false
os.queueEvent("fatmusic:song_update")
play_context.info("Next song: None.")
end
sleep(1)
end
end
)
end
main_context.debug("Start.")
local ok, err = pcall(server)
if not ok then
main_context.error(err)
end