· 6 years ago · Mar 30, 2020, 04:34 AM
1_G._p = _ENV
2local prefix = "music"
3local name = "JakeBox"
4
5settings.load ".settings"
6local key = settings.get "ytApiKey"
7local ip = "ws://"..settings.get("ip")
8
9local ws = http.websocket(ip)
10
11local queue = {}
12
13local songClass = {}
14songClass.__index = songClass
15
16local function songClass:new(title, id, slot, queuer, size)
17 local song = {}
18 setmetatable(song, songClass)
19 song.title = title
20 song.id = id
21 song.downloaded = false
22 song.slot = slot or false
23 song.queuer = queuer
24 song.downloading = false
25 song.isPlaying = false
26 song.size = size
27 return song
28end
29
30local function round(n, place)
31 if place then return round(n / place) * place end
32 return n % 1 >= 0.5 and math.ceil(n) or math.floor(n)
33end
34
35local function lengthToTime(length)
36 if not length then return "unknown" end
37 local inSeconds = round(length / 6000) --6000 bytes/second
38 local mins = math.floor(inSeconds / 60)
39 local secs = inSeconds % 60
40 return tostring(mins)..":"..(secs < 10 and "0"..tostring(secs) or tostring(secs))
41end
42
43local function where(ta, func)
44 local t = {}
45 for i,v in pairs(ta) do
46 if func(v) == true then
47 table.insert(t, v)
48 end
49 end
50 return t
51end
52
53local function reconnect()
54 while not ws or not pcall(ws.send, "test") do
55 ws = http.websocket(ip)
56 sleep(1)
57 end
58end
59
60local tape_drives = where(device, function(thing) return thing.type == "tape_drive" end)
61if #tape_drives < 2 or #tape_drives > 2 then error("needs 2 tape drives (found "..tostring(#tape_drives)..")") end
62local tape = tape_drives[1].side == "bottom" and tape_drives[1] or tape_drives[2]
63local dld = tape_drives[1].side == "bottom" and tape_drives[2] or tape_drives[1]
64
65local chest = where(device, function(thing) return thing.type == "minecraft:ironshulkerbox_crystal" end)[1]
66local loc = "top"
67
68local data_length = 16 --16 bytes
69
70local function getLength(drive, slot)
71 if slot then
72 drive.pullItems(loc, slot)
73 end
74 drive.seek(-math.huge)
75 local data = drive.read(data_length)
76 local len = tonumber(data:match("%d+"))
77 drive.seek(-math.huge)
78 if slot then
79 drive.pushItems(loc, 1, 1, slot)
80 end
81 return len
82end
83
84local function erase()
85 tape.seek(-tape.getPosition())
86 tape.write(("\0"):rep(tape.getSize())) --write NUL to every byte
87 tape.seek(-tape.getSize())
88end
89
90local function getTapeNames(lower)
91 local tapes = {}
92 for i = 1, #chest.list() do
93 if chest.getItemMeta(i) and chest.getItemMeta(i).media then --if song exists and has data
94 local t = chest.getItemMeta(i).media.label
95 if lower then t = t:lower() end
96 table.insert(tapes, t)
97 end
98 end
99 local label = tape.getLabel() --tape inside
100 if label then
101 table.insert(tapes, label)
102 end
103 return tapes
104end
105
106
107local function play(pos)
108 --tape.setVolume(1.0) --reset volume
109 tape.seek(-math.huge) --rewind
110 tape.play()
111 local song = queue[1]
112 while true do
113 local position = tape.getPosition() --setting as var for later use in a function here
114 if position >= pos then --if we are at or past the end of the song
115 tape.stop()
116 break
117 end
118 sleep()
119 end
120 tape.seek(-math.huge)
121end
122
123local function find(search, term, isSensitive)
124 if #search == 0 then return {} end --if no items then return empty table
125 if not isSensitive then term = term:lower() end
126 for i = 1, #search do
127 if not isSensitive then
128 if #{search[i]:lower():find(term, 1, true)} > 0 then
129 return { i, search[i] } --return index and full term
130 end
131 else
132 if #{search[i]:find(term, 1, true)} > 0 then
133 return { i, search[i] }
134 end
135 end
136 end
137 return {}
138end
139
140local function download(id, title, plr)
141 chatbox.tell(plr, "Downloading your song...", name)
142 ws.send("v="..id)
143 local data = ""
144 while true do
145 local ev = {os.pullEvent("websocket_message")} --wait for message
146 if ev[2] == ip and ev[3] ~= "ping" then --if from ip and not a routine ping
147 if ev[3] == "finished" then --if server done sending
148 break
149 end
150 ws.send("ack") --acknowledge that we have received the chunk
151 data = data..ev[3] --add to data
152 end
153 end
154 if data == "" then --no data received
155 chatbox.tell(plr, "Song failed to download. (timed out)", name)
156 dl = true
157 return {}
158 else
159 if not pcall(ws.send, "checking") then --if we can't send message (closed connection)
160 reconnect()
161 end
162 ws.send("done") --this tells the server to delete the dfpwm file
163 chatbox.tell(plr, "The download has completed.", name)
164 while tape.getState() == "PLAYING" do sleep() end --wait for song to finish due to bug with audio
165 dld.pullItems(loc, 1)
166 while dld.getLabel() == nil do sleep() end --wait for tape to be pulled in
167 erase()
168 local wrote = data:len() + data_length --amount written
169 local pos = tostring(wrote)
170 local empty = string.rep("\0", data_length - pos:len()) --fill the space
171 pos = pos..empty
172 dld.write(pos..data) --metadata concatenated with actual data
173
174 dld.seek(-math.huge)
175 dld.setLabel(title)
176 local slot = #(chest.list())+1 --last item in chest (if no holes)
177 dld.pushItems(loc, 1, 1, slot)
178 return { slot, wrote } --slot, size
179 end
180end
181
182local function downloadloop()
183 while true do
184 for i, song in ipairs(queue) do --do in order
185 if song.id and not song.downloaded then --if we need to download it
186 song.downloading = true --downloading
187 local data = download(song.id, song.title, song.queuer)
188 song.slot = data[1] --since we returned this in download
189 song.size = data[2] --see above comment
190 if song.slot then
191 song.downloading = false --no longer downloading
192 song.downloaded = true --the song is downloaded
193 end
194 end
195 sleep(1)
196 end
197 sleep()
198 end
199end
200
201local function skip()
202 if tape.getState() == "PLAYING" then tape.seek(queue[1].size) end
203end
204
205--[[
206now begin the commands
207because we use a main command loop, each one takes an args param
208]]--
209local function skipcmd(args)
210 if tape.getState() == "PLAYING" then
211 chatbox.tell(args[2], "Skipping the current song.", name)
212 skip()
213 else
214 chatbox.tell(args[2], "There is no song playing.", name)
215 end
216end
217
218local function addcmd(args)
219 local ti = table.concat(args[4], " "):sub(4):lower()
220 local plr = args[2]
221 local tapes = getTapeNames(false)
222
223 --local match = ti:match("https://youtube.com/watch?v=(.+)") --unsure why i have this here but it's probably slow
224
225 local enc = textutils.urlEncode(ti)
226 local response, error = http.get("https://www.googleapis.com/youtube/v3/search?q="..enc.."&maxResults=10&part=snippet&key="..key)
227 if error then chatbox.tell(plr, "The YouTube API failed to respond. ("..error..")", name)
228 else
229 local data = json.decode(response.readAll()) --json response
230 response.close()
231 local videos = data.items
232 local id = videos[1].id.videoId
233 local title = videos[1].snippet.title
234 local found = find(tapes, title, true)
235 if not found then --oh no!
236 local song = songClass:create(title, id, args[2], 0) --see songClass above for information
237 table.insert(queue, song)
238 chatbox.tell(args[2], "Added "..title, name)
239 else
240 local n = found[1]
241 local title = tapes[n]
242 local song = songClass:create(title, id, args[2], 0) --see songClass above for information
243 song.downloaded = true
244 song.slot = n
245 song.downloading = false
246 song.size = getLength(dld, n)
247 table.insert(queue, song)
248 chatbox.tell(args[2], "Added "title , name)
249 end
250 end
251end
252
253local started
254local function startcmd(args)
255 if #queue > 0 then
256 if not started then
257 started = true
258 chatbox.tell(args[2], "Beginning playback.", name)
259 repeat
260 local song = queue[1]
261 while song.downloading do sleep() end --wait for song to finish downloading
262 if song.downloaded and song.slot then
263 tape.pullItems(loc, song.slot)
264 while not tape.getLabel() do sleep() end --wait for tape to be pulled in
265 local label = tape.getLabel()
266 local pos = getLength(tape)
267 song.size = pos
268 position = pos
269 chatbox.tell(song.queuer, "Now playing: "..label, name)
270 song.playing = true
271 play(pos)
272 song.playing = false
273 tape.pushItems(loc, 1, 1, song.slot)
274 end
275 table.remove(queue, 1)
276 until #queue == 0
277
278 started = false
279 else
280 chatbox.tell(args[2], "The queue has already started.", name)
281 end
282 else chatbox.tell(args[2], "There are no songs in the queue.", name) end
283end
284
285local function pausecmd(args)
286 tape.stop()
287end
288
289local function playcmd(args)
290 tape.play()
291end
292
293local function volumecmd(args)
294 local vol = tonumber(args[4][2]) or -1 --due to the way that the chatbox api works, the fourth argument is a table; if can't convert to number then make it invalid
295 if vol >= 0 and vol <= 100 then tape.setVolume(vol / 100) end --0 to 100
296end
297
298local function queuecmd(args)
299 if #queue > 0 then
300 local list = "Queue:\n"
301 for i,v in pairs(queue) do
302 list = list..tostring(i)..". "..v.title.." (queued by "..v.queuer..")\n"
303 end
304 chatbox.tell(args[2], list, name)
305 else chatbox.tell(args[2], "The queue is empty.", name) end
306end
307
308local function helpcmd(args)
309 chatbox.tell(args[2], "Prefix: \\music \nCommands:\nadd <title>\nskip\nstart\npause\nplay\nvolume <percent>\nqueue\nhelp\nsource\nremove <number>", name)
310end
311
312local function sourcecmd(args)
313 chatbox.tell(args[2], "https://pastebin.com/raw/c1Jdf567", name) --update with source later
314end
315
316local function reconnectcmd(args)
317 ws = nil
318 reconnect()
319end
320
321local function removecmd(args)
322 local num = tonumber(args[4][2]) or 0 --if can't convert to number then make it invalid
323 if num < 1 or num > #queue then --if 0 or greater than number of songs
324 chatbox.tell(args[2], "Invalid number.", name)
325 elseif num == 1 and tape.getState() == "PLAYING" then
326 chatbox.tell(args[2], "Cannot remove the playing song.", name)
327 else
328 local title = queue[num].title
329 table.remove(queue, num)
330 chatbox.tell(args[2], "Removed "..title.." from the queue.", name)
331 end
332end
333
334local function pingws() --ping ws to make sure we stay connected
335 while true do
336 if not pcall(ws.send, "checking") then
337 reconnect()
338 end
339 ws.send "ping"
340 sleep(5)
341 end
342end
343
344local function main()
345 while true do
346 local args = {os.pullEvent "command"}
347 --addcmd, startcmd, skipcmd, pausecmd, playcmd, volumecmd, queuecmd, helpcmd, sourcecmd
348 if args[3] == "add" then
349 addcmd(args)
350 elseif args[3] == "start" then
351 startcmd(args)
352 elseif args[3] == "skip" then
353 skipcmd(args)
354 elseif args[3] == "pause" then
355 pausecmd(args)
356 elseif args[3] == "play" then
357 playcmd(args)
358 elseif args[3] == "volume" and args[4][2] then
359 volumecmd(args)
360 elseif args[3] == "queue" then
361 queuecmd(args)
362 elseif args[3] == "help" then
363 helpcmd(args)
364 elseif args[3] == "source" then
365 sourcecmd(args)
366 elseif args[3] == "remove" and args[4][2] then
367 removecmd(args)
368 else
369 chatbox.tell(args[2], "Invalid command.", name)
370 end
371 end
372end
373
374parallel.waitForAll(main, downloadloop, pingws) --main command loop, download loop, ws pings