robathome icon

MC_jukebox - Player

robathome | PRO | 10/27/25 06:23:18 PM UTC (Edited) | 0 ⭐ | 304 👁️ | Never ⏰ | []
text |

26.1 KB

|

None

|

0 👍

/

0 👎

local MODE = "ui-mirror-v1"
 -- Open Rednet on any wireless modem
local function _openRednetServer()
  local sideFound = nil
  for _, s in ipairs(rs.getSides()) do
    if peripheral.getType(s) == "modem" then sideFound = s break end
  end
  if not sideFound then
    local m = peripheral.find("modem", function(_, m) return m.isWireless and m.isWireless() end)
    if m then sideFound = peripheral.getName(m) end
  end
  assert(sideFound, "Wireless modem required on server")
  if not rednet.isOpen(sideFound) then rednet.open(sideFound) end
  return sideFound
end
_openRednetServer()
 -- Track UI clients and speaker-node clients
local clients = {}      -- [id] = {w=..., h=...}
local spk_clients = {}  -- [id] = true
 -- Broadcast helpers
local function cast(pkt)        -- to UI mirror/clients
  for id in pairs(clients) do rednet.send(id, pkt, MODE) end
end
local function castSpk(pkt)     -- to remote speaker nodes
  for id in pairs(spk_clients) do rednet.send(id, pkt, MODE) end
end
 -- term/paintutils mirror
local serverW, serverH = term.getSize()
local realTerm = term.native()
local MirrorTerm = {}
for k,v in pairs(realTerm) do MirrorTerm[k] = v end
function MirrorTerm.write(t) cast({op="write", s=t}); return realTerm.write(t) end
function MirrorTerm.blit(t,f,b) cast({op="blit", t=t,f=f,b=b}); return realTerm.blit(t,f,b) end
function MirrorTerm.clear() cast({op="clear"}); return realTerm.clear() end
function MirrorTerm.clearLine() cast({op="clearLine"}); return realTerm.clearLine() end
function MirrorTerm.setCursorPos(x,y) cast({op="setCursorPos",x=x,y=y}); return realTerm.setCursorPos(x,y) end
function MirrorTerm.setCursorBlink(b) cast({op="setCursorBlink",b=b}); return realTerm.setCursorBlink(b) end
function MirrorTerm.setTextColor(c) cast({op="setTextColor",c=c}); return realTerm.setTextColor(c) end
function MirrorTerm.setBackgroundColor(c) cast({op="setBackgroundColor",c=c}); return realTerm.setBackgroundColor(c) end
function MirrorTerm.getSize() return serverW, serverH end
term.redirect(MirrorTerm)
 local realPaint = paintutils
paintutils = {}
for k,v in pairs(realPaint) do paintutils[k] = v end
function paintutils.drawBox(x1,y1,x2,y2,col)
  cast({op="drawBox",x1=x1,y1=y1,x2=x2,y2=y2,col=col})
  return realPaint.drawBox(x1,y1,x2,y2,col)
end
function paintutils.drawFilledBox(x1,y1,x2,y2,col)
  cast({op="drawFilledBox",x1=x1,y1=y1,x2=x2,y2=y2,col=col})
  return realPaint.drawFilledBox(x1,y1,x2,y2,col)
end
 -- Build compact status for controllers
local function _copyQueue()
  local q, out = rawget(_G, "queue"), {}
  if type(q) == "table" then
    for i,item in ipairs(q) do
      out[i] = { name = item.name, artist = item.artist }
    end
  end
  return out
end
local function _copyResults()
  local r, out = rawget(_G, "search_results"), nil
  if type(r) == "table" then
    out = {}
    for i,item in ipairs(r) do
      out[i] = { name = item.name, artist = item.artist, type = item.type or "track" }
    end
  end
  return out
end
local function buildStatus()
  local now = rawget(_G, "now_playing")
  return {
    op = "STATUS",
    now = now and { name = now.name, artist = now.artist } or nil,
    playing = not not rawget(_G, "playing"),
    looping = tonumber(rawget(_G, "looping")) or 0,
    volume = tonumber(rawget(_G, "volume")) or 1.0,
    is_loading = not not rawget(_G, "is_loading"),
    is_error = not not rawget(_G, "is_error"),
    queue = _copyQueue(),
    last_search = rawget(_G, "last_search"),
    search_results = _copyResults(),
  }
end
 -- Remote audio streamer (send DFPWM chunks to speaker nodes)
local function netAudioSend(dfpwm_str, vol)
  if not dfpwm_str or #dfpwm_str == 0 then return end
  if next(spk_clients) == nil then return end
  local max = 4096
  local seq = os.epoch("utc")
  castSpk({op="AUDIO_BEGIN", seq=seq, vol=vol})
  local i = 1
  while i <= #dfpwm_str do
    castSpk({op="AUDIO_PART", seq=seq, data=dfpwm_str:sub(i, i+max-1)})
    i = i + max
  end
  castSpk({op="AUDIO_END", seq=seq})
end
 -- Safe speaker stop
local function _stopSpeakers()
  local sp = rawget(_G, "speakers")
  if type(sp) == "table" then
    for _, s in ipairs(sp) do pcall(function() s.stop() end) end
    os.queueEvent("playback_stopped")
  end
end
 -- Execute controller commands
local function execCommand(msg)
  local cmd = msg.cmd
  if cmd == "PLAY_TOGGLE" then
    if _G.playing then
      _G.playing = false
      _stopSpeakers()
      _G.playing_id = nil
      _G.is_loading = false
      _G.is_error = false
    elseif _G.now_playing ~= nil then
      _G.playing_id = nil
      _G.playing = true
      _G.is_error = false
    elseif type(_G.queue) == "table" and #_G.queue > 0 then
      _G.now_playing = _G.queue[1]
      table.remove(_G.queue, 1)
      _G.playing_id = nil
      _G.playing = true
      _G.is_error = false
    end
    os.queueEvent("audio_update")
  elseif cmd == "STOP" then
    _G.playing = false
    _stopSpeakers()
    _G.playing_id = nil
    _G.is_loading = false
    _G.is_error = false
    os.queueEvent("audio_update")
  elseif cmd == "SKIP" then
    if _G.playing then _stopSpeakers() end
    if type(_G.queue) == "table" and #_G.queue > 0 then
      if _G.looping == 1 and _G.now_playing then
        table.insert(_G.queue, _G.now_playing)
      end
      _G.now_playing = _G.queue[1]
      table.remove(_G.queue, 1)
      _G.playing_id = nil
    else
      _G.now_playing = nil
      _G.playing = false
      _G.is_loading = false
      _G.is_error = false
      _G.playing_id = nil
    end
    os.queueEvent("audio_update")
  elseif cmd == "LOOP_NEXT" then
    _G.looping = ((_G.looping or 0) + 1) % 3
  elseif cmd == "VOL_SET" then
    local v = tonumber(msg.value)
    if v then _G.volume = math.max(0, math.min(3, v)) end
  elseif cmd == "SEARCH" and type(msg.q) == "string" then
    local api = rawget(_G, "api_base_url")
    local ver = rawget(_G, "version")
    if api and ver then
      _G.last_search = msg.q
      _G.last_search_url = api .. "?v=" .. ver .. "&search=" .. textutils.urlEncode(_G.last_search)
      http.request(_G.last_search_url)
      _G.search_results = nil
      _G.search_error = false
    end
  elseif cmd == "PLAY_NOW" and tonumber(msg.idx) and type(_G.search_results) == "table" then
    local i = tonumber(msg.idx)
    local item = _G.search_results[i]
    if item then
      _stopSpeakers()
      _G.playing = true
      _G.is_error = false
      _G.playing_id = nil
      if item.type == "playlist" and item.playlist_items and #item.playlist_items > 0 then
        _G.now_playing = item.playlist_items[1]
        _G.queue = {}
        for j = 2, #item.playlist_items do table.insert(_G.queue, item.playlist_items[j]) end
      else
        _G.now_playing = item
      end
      os.queueEvent("audio_update")
    end
  elseif cmd == "PLAY_NEXT" and tonumber(msg.idx) and type(_G.search_results) == "table" then
    local i = tonumber(msg.idx)
    local item = _G.search_results[i]
    if item then
      _G.queue = _G.queue or {}
      if item.type == "playlist" and item.playlist_items then
        for j = #item.playlist_items, 1, -1 do table.insert(_G.queue, 1, item.playlist_items[j]) end
      else
        table.insert(_G.queue, 1, item)
      end
      os.queueEvent("audio_update")
    end
  elseif cmd == "ADD_QUEUE" and tonumber(msg.idx) and type(_G.search_results) == "table" then
    local i = tonumber(msg.idx)
    local item = _G.search_results[i]
    if item then
      _G.queue = _G.queue or {}
      if item.type == "playlist" and item.playlist_items then
        for j = 1, #item.playlist_items do table.insert(_G.queue, item.playlist_items[j]) end
      else
        table.insert(_G.queue, item)
      end
      os.queueEvent("audio_update")
    end
  elseif cmd == "PLAY_FROM_QUEUE_IDX" and tonumber(msg.idx) and type(_G.queue) == "table" then
    local i = tonumber(msg.idx)
    if i >= 1 and i <= #_G.queue then
      _stopSpeakers()
      _G.now_playing = _G.queue[i]
      table.remove(_G.queue, i)
      _G.playing_id = nil
      _G.playing = true
      _G.is_error = false
      os.queueEvent("audio_update")
    end
  elseif cmd == "REMOVE_QUEUE_IDX" and tonumber(msg.idx) and type(_G.queue) == "table" then
    local i = tonumber(msg.idx)
    if i >= 1 and i <= #_G.queue then
      table.remove(_G.queue, i)
      os.queueEvent("audio_update")
    end
  end
  os.queueEvent("redraw_screen")
end
 -- Listeners
function mirrorHelloLoop()
  cast({op="screen", w=serverW, h=serverH})
  while true do
    local id, msg, proto = rednet.receive(MODE)
    if proto == MODE and type(msg) == "table" then
      if msg.op == "HELLO" and msg.role == "speaker" then
        spk_clients[id] = true
        rednet.send(id, { op="HELLO_ACK_SPK" }, MODE)
      elseif msg.op == "HELLO" and msg.w and msg.h then
        clients[id] = { w = msg.w, h = msg.h }
        rednet.send(id, { op="HELLO_ACK", w=serverW, h=serverH }, MODE)
        os.queueEvent("redraw_screen")
      elseif msg.op == "CLICK" then
        os.queueEvent("mouse_click", msg.button or 1, msg.x, msg.y)
      elseif msg.op == "DRAG" then
        os.queueEvent("mouse_drag", msg.button or 1, msg.x, msg.y)
      elseif msg.op == "SCROLL" then
        os.queueEvent("mouse_scroll", msg.dir or 0, msg.x, msg.y)
      elseif msg.op == "CMD" then
        execCommand(msg)
      elseif msg.op == "STATUS_REQ" then
        rednet.send(id, buildStatus(), MODE)
      end
    end
  end
end
 function mirrorStatusLoop()
  while true do
    cast(buildStatus())
    sleep(0.5)
  end
end
-- =====================
-- END PATCH
-- =====================
  -- =====================
-- ORIGINAL SERVER PROGRAM (with minor edits to integrate streaming + threads)
-- =====================
local api_base_url = "https://ipod-2to6_MAGYNA-uc.a.run.app/"
local version = "2.1"
 local width, height = term.getSize()
local tab = 1
 local waiting_for_input = false
local last_search = nil
local last_search_url = nil
local search_results = nil
local search_error = false
local in_search_result = false
local clicked_result = nil
 local playing = false
local queue = {}
local now_playing = nil
local looping = 0
local volume = 1.5
 local playing_id = nil
local last_download_url = nil
local playing_status = 0
local is_loading = false
local is_error = false;
 local player_handle = nil
local start = nil
local pcm = nil
local size = nil
local decoder = require "cc.audio.dfpwm".make_decoder()
local needs_next_chunk = 0
local buffer
 local speakers = { peripheral.find("speaker") }
if #speakers == 0 then
  print("No local speakers attached; streaming only.")
end
 function redrawScreen()
  if waiting_for_input then return end
  term.setCursorBlink(false); term.setBackgroundColor(colors.black); term.clear()
  term.setCursorPos(1,1); term.setBackgroundColor(colors.gray); term.clearLine()
  tabs = {" Now Playing ", " Search "}
  for i=1,#tabs,1 do
    if tab == i then term.setTextColor(colors.black) term.setBackgroundColor(colors.white)
    else term.setTextColor(colors.white) term.setBackgroundColor(colors.gray) end
    term.setCursorPos((math.floor((width/#tabs)*(i-0.5)))-math.ceil(#tabs[i]/2)+1, 1); term.write(tabs[i])
  end
  if tab == 1 then drawNowPlaying() elseif tab == 2 then drawSearch() end
end
 function drawNowPlaying()
  if now_playing ~= nil then
    term.setBackgroundColor(colors.black); term.setTextColor(colors.white); term.setCursorPos(2,3); term.write(now_playing.name)
    term.setTextColor(colors.lightGray); term.setCursorPos(2,4); term.write(now_playing.artist)
  else
    term.setBackgroundColor(colors.black); term.setTextColor(colors.lightGray); term.setCursorPos(2,3); term.write("Not playing")
  end
  if is_loading == true then term.setTextColor(colors.gray); term.setBackgroundColor(colors.black); term.setCursorPos(2,5); term.write("Loading...")
  elseif is_error == true then term.setTextColor(colors.red); term.setBackgroundColor(colors.black); term.setCursorPos(2,5); term.write("Network error") end
  term.setTextColor(colors.white); term.setBackgroundColor(colors.gray)
  if playing then term.setCursorPos(2, 6); term.write(" Stop ") else
    if now_playing ~= nil or #queue > 0 then term.setTextColor(colors.white) term.setBackgroundColor(colors.gray) else term.setTextColor(colors.lightGray) term.setBackgroundColor(colors.gray) end
    term.setCursorPos(2, 6); term.write(" Play ")
  end
  if now_playing ~= nil or #queue > 0 then term.setTextColor(colors.white) term.setBackgroundColor(colors.gray) else term.setTextColor(colors.lightGray) term.setBackgroundColor(colors.gray) end
  term.setCursorPos(2 + 7, 6); term.write(" Skip ")
  if looping ~= 0 then term.setTextColor(colors.black) term.setBackgroundColor(colors.white) else term.setTextColor(colors.white) term.setBackgroundColor(colors.gray) end
  term.setCursorPos(2 + 7 + 7, 6)
  if looping == 0 then term.write(" Loop Off ") elseif looping == 1 then term.write(" Loop Queue ") else term.write(" Loop Song ") end
  term.setCursorPos(2,8); paintutils.drawBox(2,8,25,8,colors.gray)
  local wv = math.floor(24 * (volume / 3) + 0.5)-1
  if not (wv == -1) then paintutils.drawBox(2,8,2+wv,8,colors.white) end
  if volume < 0.6 then term.setCursorPos(2+wv+2,8); term.setBackgroundColor(colors.gray); term.setTextColor(colors.white)
  else term.setCursorPos(2+wv-3-(volume == 3 and 1 or 0),8); term.setBackgroundColor(colors.white); term.setTextColor(colors.black) end
  term.write(math.floor(100 * (volume / 3) + 0.5) .. "%")
  if #queue > 0 then
    term.setBackgroundColor(colors.black)
    for i=1,#queue do term.setTextColor(colors.white); term.setCursorPos(2,10 + (i-1)*2); term.write(queue[i].name); term.setTextColor(colors.lightGray); term.setCursorPos(2,11 + (i-1)*2); term.write(queue[i].artist) end
  end
end
 function drawSearch()
  paintutils.drawFilledBox(2,3,width-1,5,colors.lightGray); term.setBackgroundColor(colors.lightGray); term.setCursorPos(3,4); term.setTextColor(colors.black); term.write(last_search or "Search...")
  if search_results ~= nil then
    term.setBackgroundColor(colors.black)
    for i=1,#search_results do term.setTextColor(colors.white); term.setCursorPos(2,7 + (i-1)*2); term.write(search_results[i].name); term.setTextColor(colors.lightGray); term.setCursorPos(2,8 + (i-1)*2); term.write(search_results[i].artist) end
  else
    term.setCursorPos(2,7); term.setBackgroundColor(colors.black)
    if search_error == true then term.setTextColor(colors.red); term.write("Network error")
    elseif last_search_url ~= nil then term.setTextColor(colors.lightGray); term.write("Searching...")
    else term.setCursorPos(1,7); term.setTextColor(colors.lightGray); print("Tip: You can paste YouTube video or playlist links.") end
  end
  if in_search_result == true then
    term.setBackgroundColor(colors.black); term.clear(); term.setCursorPos(2,2); term.setTextColor(colors.white); term.write(search_results[clicked_result].name)
    term.setCursorPos(2,3); term.setTextColor(colors.lightGray); term.write(search_results[clicked_result].artist)
    term.setBackgroundColor(colors.gray); term.setTextColor(colors.white)
    term.setCursorPos(2,6); term.clearLine(); term.write("Play now")
    term.setCursorPos(2,8); term.clearLine(); term.write("Play next")
    term.setCursorPos(2,10); term.clearLine(); term.write("Add to queue")
    term.setCursorPos(2,13); term.clearLine(); term.write("Cancel")
  end
end
 function uiLoop()
  redrawScreen()
  while true do
    if waiting_for_input then
      parallel.waitForAny(
        function()
          term.setCursorPos(3,4); term.setBackgroundColor(colors.white); term.setTextColor(colors.black)
          local input = read()
          if string.len(input) > 0 then
            last_search = input; last_search_url = api_base_url .. "?v=" .. version .. "&search=" .. textutils.urlEncode(input)
            http.request(last_search_url); search_results = nil; search_error = false
          else last_search = nil; last_search_url = nil; search_results = nil; search_error = false end
          waiting_for_input = false; os.queueEvent("redraw_screen")
        end,
        function()
          while waiting_for_input do
            local event, button, x, y = os.pullEvent("mouse_click")
            if y < 3 or y > 5 or x < 2 or x > width-1 then waiting_for_input = false; os.queueEvent("redraw_screen"); break end
          end
        end
      )
    else
      parallel.waitForAny(
        function()
          local event, button, x, y = os.pullEvent("mouse_click")
          if button == 1 then
            if in_search_result == false and y == 1 then if x < width/2 then tab = 1 else tab = 2 end; redrawScreen() end
            if tab == 2 and in_search_result == false then
              if y >= 3 and y <= 5 and x >= 1 and x <= width-1 then paintutils.drawFilledBox(2,3,width-1,5,colors.white); term.setBackgroundColor(colors.white); waiting_for_input = true end
              if search_results then
                for i=1,#search_results do
                  if y == 7 + (i-1)*2 or y == 8 + (i-1)*2 then
                    term.setBackgroundColor(colors.white); term.setTextColor(colors.black); term.setCursorPos(2,7 + (i-1)*2); term.clearLine(); term.write(search_results[i].name)
                    term.setTextColor(colors.gray); term.setCursorPos(2,8 + (i-1)*2); term.clearLine(); term.write(search_results[i].artist)
                    sleep(0.2); in_search_result = true; clicked_result = i; redrawScreen()
                  end
                end
              end
            elseif tab == 2 and in_search_result == true then
              term.setBackgroundColor(colors.white); term.setTextColor(colors.black)
              if y == 6 then
                term.setCursorPos(2,6); term.clearLine(); term.write("Play now"); sleep(0.2)
                in_search_result = false; for _, speaker in ipairs(speakers) do speaker.stop() os.queueEvent("playback_stopped") end
                playing = true; is_error = false; playing_id = nil
                if search_results[clicked_result].type == "playlist" then
                  now_playing = search_results[clicked_result].playlist_items[1]; queue = {}
                  if #search_results[clicked_result].playlist_items > 1 then for i=2, #search_results[clicked_result].playlist_items do table.insert(queue, search_results[clicked_result].playlist_items[i]) end end
                else now_playing = search_results[clicked_result] end
                os.queueEvent("audio_update")
              end
              if y == 8 then
                term.setCursorPos(2,8); term.clearLine(); term.write("Play next"); sleep(0.2)
                in_search_result = false
                if search_results[clicked_result].type == "playlist" then for i = #search_results[clicked_result].playlist_items, 1, -1 do table.insert(queue, 1, search_results[clicked_result].playlist_items[i]) end
                else table.insert(queue, 1, search_results[clicked_result]) end
                os.queueEvent("audio_update")
              end
              if y == 10 then
                term.setCursorPos(2,10); term.clearLine(); term.write("Add to queue"); sleep(0.2)
                in_search_result = false
                if search_results[clicked_result].type == "playlist" then for i = 1, #search_results[clicked_result].playlist_items do table.insert(queue, search_results[clicked_result].playlist_items[i]) end
                else table.insert(queue, search_results[clicked_result]) end
                os.queueEvent("audio_update")
              end
              if y == 13 then term.setCursorPos(2,13); term.clearLine(); term.write("Cancel"); sleep(0.2); in_search_result = false end
              redrawScreen()
            elseif tab == 1 and in_search_result == false then
              if y == 6 then
                if x >= 2 and x < 2 + 6 then
                  if playing or now_playing ~= nil or #queue > 0 then term.setBackgroundColor(colors.white); term.setTextColor(colors.black); term.setCursorPos(2, 6); if playing then term.write(" Stop ") else term.write(" Play ") end; sleep(0.2) end
                  if playing then
                    playing = false; for _, speaker in ipairs(speakers) do speaker.stop() os.queueEvent("playback_stopped") end; playing_id = nil; is_loading = false; is_error = false; os.queueEvent("audio_update")
                  elseif now_playing ~= nil then playing_id = nil; playing = true; is_error = false; os.queueEvent("audio_update")
                  elseif #queue > 0 then now_playing = queue[1]; table.remove(queue, 1); playing_id = nil; playing = true; is_error = false; os.queueEvent("audio_update") end
                end
                if x >= 2 + 7 and x < 2 + 7 + 6 then
                  if now_playing ~= nil or #queue > 0 then
                    term.setBackgroundColor(colors.white); term.setTextColor(colors.black); term.setCursorPos(2 + 7, 6); term.write(" Skip "); sleep(0.2)
                    is_error = false
                    if playing then for _, speaker in ipairs(speakers) do speaker.stop() os.queueEvent("playback_stopped") end end
                    if #queue > 0 then if looping == 1 then table.insert(queue, now_playing) end; now_playing = queue[1]; table.remove(queue, 1); playing_id = nil
                    else now_playing = nil; playing = false; is_loading = false; is_error = false; playing_id = nil end
                    os.queueEvent("audio_update")
                  end
                end
                if x >= 2 + 7 + 7 and x < 2 + 7 + 7 + 12 then if looping == 0 then looping = 1 elseif looping == 1 then looping = 2 else looping = 0 end end
              end
              if y == 8 then if x >= 1 and x < 2 + 24 then volume = (x - 1) / 24 * 3 end end
              redrawScreen()
            end
          end
        end,
        function() local event, button, x, y = os.pullEvent("mouse_drag"); if button == 1 and tab == 1 and in_search_result == false then if y >= 7 and y <= 9 and x >= 1 and x < 2 + 24 then volume = (x - 1) / 24 * 3 end; redrawScreen() end end,
        function() local event = os.pullEvent("redraw_screen"); redrawScreen() end
      )
    end
  end
end
 function audioLoop()
  while true do
    if playing and now_playing then
      local thisnowplayingid = now_playing.id
      if playing_id ~= thisnowplayingid then
        playing_id = thisnowplayingid
        last_download_url = api_base_url .. "?v=" .. version .. "&id=" .. textutils.urlEncode(playing_id)
        playing_status = 0; needs_next_chunk = 1
        http.request({url = last_download_url, binary = true}); is_loading = true
        os.queueEvent("redraw_screen"); os.queueEvent("audio_update")
      elseif playing_status == 1 and needs_next_chunk == 1 then
        while true do
          local chunk = player_handle.read(size)
          if not chunk then
            if looping == 2 or (looping == 1 and #queue == 0) then playing_id = nil
            elseif looping == 1 and #queue > 0 then table.insert(queue, now_playing); now_playing = queue[1]; table.remove(queue, 1); playing_id = nil
            else if #queue > 0 then now_playing = queue[1]; table.remove(queue, 1); playing_id = nil else now_playing = nil; playing = false; playing_id = nil; is_loading = false; is_error = false end end
            os.queueEvent("redraw_screen"); if player_handle and player_handle.close then player_handle.close() end; needs_next_chunk = 0; break
          else
            if start then chunk, start = start .. chunk, nil; size = size + 4 end
            buffer = decoder(chunk)         -- local playback
            netAudioSend(chunk, volume)     -- NEW: remote speaker broadcast
            local fn = {}
            for i, speaker in ipairs(speakers) do 
              fn[i] = function()
                local name = peripheral.getName(speaker)
                if #speakers > 1 then
                  if speaker.playAudio(buffer, volume) then
                    parallel.waitForAny(
                      function() repeat until select(2, os.pullEvent("speaker_audio_empty")) == name end,
                      function() local event = os.pullEvent("playback_stopped") return end
                    )
                    if not playing or playing_id ~= thisnowplayingid then return end
                  end
                else
                  while not speaker.playAudio(buffer, volume) do
                    parallel.waitForAny(
                      function() repeat until select(2, os.pullEvent("speaker_audio_empty")) == name end,
                      function() local event = os.pullEvent("playback_stopped") return end
                    )
                    if not playing or playing_id ~= thisnowplayingid then return end
                  end
                end
                if not playing or playing_id ~= thisnowplayingid then return end
              end
            end
            local ok, err = pcall(parallel.waitForAll, table.unpack(fn))
            if not ok then needs_next_chunk = 2; is_error = true; break end
            if not playing or playing_id ~= thisnowplayingid then break end
          end
        end
        os.queueEvent("audio_update")
      end
    end
    os.pullEvent("audio_update")
  end
end
 function httpLoop()
  while true do
    parallel.waitForAny(
      function()
        local event, url, handle = os.pullEvent("http_success")
        if url == last_search_url then search_results = textutils.unserialiseJSON(handle.readAll()); os.queueEvent("redraw_screen") end
        if url == last_download_url then is_loading = false; player_handle = handle; start = handle.read(4); size = 16 * 1024 - 4; playing_status = 1; os.queueEvent("redraw_screen"); os.queueEvent("audio_update") end
      end,
      function()
        local event, url = os.pullEvent("http_failure")
        if url == last_search_url then search_error = true; os.queueEvent("redraw_screen") end
        if url == last_download_url then is_loading = false; is_error = true; playing = false; playing_id = nil; os.queueEvent("redraw_screen"); os.queueEvent("audio_update") end
      end
    )
  end
end
 parallel.waitForAny(uiLoop, audioLoop, httpLoop, mirrorHelloLoop, mirrorStatusLoop)

Comments