Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- local API_BASE_URL = "https://ipod-2to6magyna-uc.a.run.app/"
- local VERSION = "2.1"
- local width, height = term.getSize()
- local State = {
- ui = {
- tab = 1,
- waiting_for_input = false,
- in_search_result = false,
- clicked_result = nil,
- },
- net = {
- last_search = nil,
- last_search_url = nil,
- search_results = nil,
- search_error = false,
- last_download_url = nil,
- is_loading = false,
- is_error = false
- },
- player = {
- playing = false,
- playing_id = nil,
- status = 0,
- queue = {},
- now_playing = nil,
- looping = 0,
- volume = 1.5,
- handle = nil,
- needs_next_chunk = 0
- }
- }
- local start_chunk, pcm, size, buffer
- local decoder = require("cc.audio.dfpwm").make_decoder()
- local speakers = { peripheral.find("speaker") }
- if #speakers == 0 then
- error("No speakers attached.", 0)
- end
- local accent_color = colors.lightBlue
- local bg_color = colors.black
- local panel_color = colors.gray
- local text_color = colors.white
- local subtext_color = colors.lightGray
- local function stopPlayback()
- State.player.playing = false
- for _, speaker in ipairs(speakers) do
- speaker.stop()
- end
- os.queueEvent("playback_stopped")
- end
- local function playSong(songData)
- stopPlayback()
- State.player.playing = true
- State.net.is_error = false
- State.player.playing_id = nil
- State.player.now_playing = songData
- os.queueEvent("audio_update")
- end
- local function addToQueue(item, playNext)
- local itemsToAdd = (item.type == "playlist") and item.playlist_items or {item}
- if playNext then
- for i = #itemsToAdd, 1, -1 do
- table.insert(State.player.queue, 1, itemsToAdd[i])
- end
- else
- for i = 1, #itemsToAdd do
- table.insert(State.player.queue, itemsToAdd[i])
- end
- end
- os.queueEvent("audio_update")
- end
- local function drawButton(x, y, text, active)
- term.setCursorPos(x, y)
- if active then
- term.setTextColor(bg_color)
- term.setBackgroundColor(accent_color)
- else
- term.setTextColor(text_color)
- term.setBackgroundColor(panel_color)
- end
- term.write(text)
- term.setBackgroundColor(bg_color)
- end
- local function drawTabs()
- term.setCursorPos(1, 1)
- term.setBackgroundColor(panel_color)
- term.clearLine()
- local tabs = {" Now Playing ", " Search "}
- for i = 1, #tabs do
- local isActive = (State.ui.tab == i)
- local xPos = (math.floor((width / #tabs) * (i - 0.5))) - math.ceil(#tabs[i] / 2) + 1
- drawButton(xPos, 1, tabs[i], isActive)
- end
- end
- local function drawNowPlaying()
- term.setBackgroundColor(bg_color)
- if State.player.now_playing then
- term.setTextColor(accent_color)
- term.setCursorPos(2, 3)
- term.write(State.player.now_playing.name)
- term.setTextColor(text_color)
- term.setCursorPos(2, 4)
- term.write(State.player.now_playing.artist)
- else
- term.setTextColor(subtext_color)
- term.setCursorPos(2, 3)
- term.write("Not playing")
- end
- term.setCursorPos(2, 5)
- if State.net.is_loading then
- term.setTextColor(colors.yellow)
- term.write("Loading stream...")
- elseif State.net.is_error then
- term.setTextColor(colors.red)
- term.write("Network error")
- else
- term.clearLine()
- end
- local hasMedia = (State.player.now_playing ~= nil or #State.player.queue > 0)
- drawButton(2, 6, State.player.playing and " Stop " or " Play ", State.player.playing)
- term.setCursorPos(2 + 7, 6)
- term.setTextColor(hasMedia and text_color or subtext_color)
- term.setBackgroundColor(panel_color)
- term.write(" Skip ")
- local loopText = State.player.looping == 0 and " Loop Off " or (State.player.looping == 1 and " Loop Queue " or " Loop Song ")
- drawButton(2 + 7 + 7, 6, loopText, State.player.looping ~= 0)
- term.setCursorPos(2, 8)
- paintutils.drawBox(2, 8, 25, 8, panel_color)
- local volWidth = math.floor(24 * (State.player.volume / 3) + 0.5) - 1
- if volWidth >= 0 then
- paintutils.drawBox(2, 8, 2 + volWidth, 8, accent_color)
- end
- local volTextPos = (State.player.volume < 0.6) and (2 + volWidth + 2) or (2 + volWidth - 3 - (State.player.volume == 3 and 1 or 0))
- term.setCursorPos(volTextPos, 8)
- term.setBackgroundColor(State.player.volume < 0.6 and panel_color or accent_color)
- term.setTextColor(State.player.volume < 0.6 and text_color or bg_color)
- term.write(math.floor(100 * (State.player.volume / 3) + 0.5) .. "%")
- if #State.player.queue > 0 then
- term.setBackgroundColor(bg_color)
- term.setTextColor(subtext_color)
- term.setCursorPos(2, 10)
- term.write("Up Next:")
- for i = 1, math.min(#State.player.queue, 4) do
- term.setTextColor(text_color)
- term.setCursorPos(2, 10 + i * 2)
- term.write("- " .. string.sub(State.player.queue[i].name, 1, width - 4))
- term.setTextColor(subtext_color)
- term.setCursorPos(4, 11 + i * 2)
- term.write(string.sub(State.player.queue[i].artist, 1, width - 6))
- end
- end
- end
- local function drawSearch()
- paintutils.drawFilledBox(2, 3, width - 1, 5, panel_color)
- term.setBackgroundColor(panel_color)
- term.setTextColor(accent_color)
- term.setCursorPos(3, 4)
- if State.net.last_search then
- term.write(string.sub(State.net.last_search, 1, width - 4))
- else
- term.write("Search or paste URL...")
- end
- term.setBackgroundColor(bg_color)
- if State.ui.in_search_result then
- term.clear()
- local result = State.net.search_results[State.ui.clicked_result]
- term.setCursorPos(2, 2)
- term.setTextColor(accent_color)
- term.write(string.sub(result.name, 1, width - 2))
- term.setCursorPos(2, 3)
- term.setTextColor(text_color)
- term.write(string.sub(result.artist, 1, width - 2))
- local options = {"Play now", "Play next", "Add to queue", "Cancel"}
- local yPos = {6, 8, 10, 13}
- for i, opt in ipairs(options) do
- term.setCursorPos(2, yPos[i])
- term.setBackgroundColor(panel_color)
- term.setTextColor(text_color)
- term.clearLine()
- term.write(" " .. opt .. " ")
- end
- else
- if State.net.search_results then
- for i = 1, #State.net.search_results do
- term.setTextColor(text_color)
- term.setCursorPos(2, 7 + (i - 1) * 2)
- term.write(string.sub(State.net.search_results[i].name, 1, width - 2))
- term.setTextColor(subtext_color)
- term.setCursorPos(2, 8 + (i - 1) * 2)
- term.write(string.sub(State.net.search_results[i].artist, 1, width - 2))
- end
- else
- term.setCursorPos(2, 7)
- if State.net.search_error then
- term.setTextColor(colors.red)
- term.write("Network error")
- elseif State.net.last_search_url then
- term.setTextColor(accent_color)
- term.write("Searching...")
- else
- term.setCursorPos(2, 7)
- term.setTextColor(subtext_color)
- term.write("Waiting for search query...")
- end
- end
- end
- end
- function redrawScreen()
- if State.ui.waiting_for_input then return end
- term.setCursorBlink(false)
- term.setBackgroundColor(bg_color)
- term.clear()
- drawTabs()
- if State.ui.tab == 1 then
- drawNowPlaying()
- elseif State.ui.tab == 2 then
- drawSearch()
- end
- local watermark = "Criado por Luke Arwolf"
- if math.floor(os.clock() / 10) % 2 == 0 then
- watermark = "Criado por Luke Arwolf"
- term.setTextColor(accent_color)
- else
- term.setTextColor(subtext_color)
- end
- term.setCursorPos(width - #watermark + 1, height)
- term.setBackgroundColor(bg_color)
- term.write(watermark)
- end
- local function handleSearchInputClick(x, y)
- if y >= 3 and y <= 5 and x >= 1 and x <= width - 1 then
- paintutils.drawFilledBox(2, 3, width - 1, 5, text_color)
- State.ui.waiting_for_input = true
- return true
- end
- return false
- end
- local function handleSearchResultClick(x, y)
- if not State.net.search_results then return false end
- for i = 1, #State.net.search_results do
- if y == 7 + (i - 1) * 2 or y == 8 + (i - 1) * 2 then
- drawButton(2, 7 + (i - 1) * 2, string.sub(State.net.search_results[i].name, 1, width - 2), true)
- sleep(0.1)
- State.ui.in_search_result = true
- State.ui.clicked_result = i
- redrawScreen()
- return true
- end
- end
- return false
- end
- local function handleSongMenuClick(y)
- local result = State.net.search_results[State.ui.clicked_result]
- if y == 6 then
- if result.type == "playlist" then
- State.player.queue = {}
- if #result.playlist_items > 1 then
- for i = 2, #result.playlist_items do table.insert(State.player.queue, result.playlist_items[i]) end
- end
- playSong(result.playlist_items[1])
- else
- playSong(result)
- end
- elseif y == 8 then
- addToQueue(result, true)
- elseif y == 10 then
- addToQueue(result, false)
- elseif y ~= 13 then
- return
- end
- sleep(0.1)
- State.ui.in_search_result = false
- redrawScreen()
- end
- local function handleControlsClick(x, y)
- if y == 6 then
- if x >= 2 and x < 8 then
- if State.player.playing then
- stopPlayback()
- State.player.playing_id = nil
- State.net.is_loading = false
- elseif State.player.now_playing or #State.player.queue > 0 then
- if not State.player.now_playing then
- State.player.now_playing = table.remove(State.player.queue, 1)
- end
- State.player.playing = true
- State.player.playing_id = nil
- os.queueEvent("audio_update")
- end
- elseif x >= 9 and x < 15 and (State.player.now_playing or #State.player.queue > 0) then
- stopPlayback()
- if #State.player.queue > 0 then
- if State.player.looping == 1 then table.insert(State.player.queue, State.player.now_playing) end
- State.player.now_playing = table.remove(State.player.queue, 1)
- State.player.playing_id = nil
- State.player.playing = true
- else
- State.player.now_playing = nil
- State.player.playing_id = nil
- State.net.is_loading = false
- end
- os.queueEvent("audio_update")
- elseif x >= 16 and x < 28 then
- State.player.looping = (State.player.looping + 1) % 3
- end
- elseif y >= 7 and y <= 9 then
- if x >= 1 and x < 26 then
- State.player.volume = (x - 1) / 24 * 3
- end
- end
- redrawScreen()
- end
- function uiLoop()
- local timer = os.startTimer(1)
- redrawScreen()
- while true do
- if State.ui.waiting_for_input then
- parallel.waitForAny(
- function()
- term.setCursorPos(3, 4)
- term.setBackgroundColor(text_color)
- term.setTextColor(bg_color)
- local input = read()
- if string.len(input) > 0 then
- State.net.last_search = input
- State.net.last_search_url = API_BASE_URL .. "?v=" .. VERSION .. "&search=" .. textutils.urlEncode(input)
- http.request(State.net.last_search_url)
- State.net.search_results = nil
- State.net.search_error = false
- end
- State.ui.waiting_for_input = false
- os.queueEvent("redraw_screen")
- end,
- function()
- while State.ui.waiting_for_input do
- local _, _, mx, my = os.pullEvent("mouse_click")
- if my < 3 or my > 5 or mx < 2 or mx > width - 1 then
- State.ui.waiting_for_input = false
- os.queueEvent("redraw_screen")
- break
- end
- end
- end
- )
- else
- local event, p1, p2, p3 = os.pullEvent()
- if event == "mouse_click" and p1 == 1 then
- local x, y = p2, p3
- if not State.ui.in_search_result and y == 1 then
- State.ui.tab = (x < width / 2) and 1 or 2
- redrawScreen()
- elseif State.ui.tab == 2 then
- if not State.ui.in_search_result then
- if not handleSearchInputClick(x, y) then handleSearchResultClick(x, y) end
- else
- handleSongMenuClick(y)
- end
- elseif State.ui.tab == 1 then
- handleControlsClick(x, y)
- end
- elseif event == "mouse_drag" and p1 == 1 and State.ui.tab == 1 and not State.ui.in_search_result then
- if p3 >= 7 and p3 <= 9 and p2 >= 1 and p2 < 26 then
- State.player.volume = (p2 - 1) / 24 * 3
- redrawScreen()
- end
- elseif event == "timer" and p1 == timer then
- redrawScreen()
- timer = os.startTimer(1)
- elseif event == "redraw_screen" then
- redrawScreen()
- end
- end
- end
- end
- function audioLoop()
- while true do
- if State.player.playing and State.player.now_playing then
- local current_id = State.player.now_playing.id
- if State.player.playing_id ~= current_id then
- State.player.playing_id = current_id
- State.net.last_download_url = API_BASE_URL .. "?v=" .. VERSION .. "&id=" .. textutils.urlEncode(current_id)
- State.player.status = 0
- State.player.needs_next_chunk = 1
- http.request({url = State.net.last_download_url, binary = true})
- State.net.is_loading = true
- os.queueEvent("redraw_screen")
- os.queueEvent("audio_update")
- elseif State.player.status == 1 and State.player.needs_next_chunk == 1 then
- while true do
- local chunk = State.player.handle.read(size)
- if not chunk then
- if State.player.looping == 2 or (State.player.looping == 1 and #State.player.queue == 0) then
- State.player.playing_id = nil
- elseif State.player.looping == 1 and #State.player.queue > 0 then
- table.insert(State.player.queue, State.player.now_playing)
- State.player.now_playing = table.remove(State.player.queue, 1)
- State.player.playing_id = nil
- else
- if #State.player.queue > 0 then
- State.player.now_playing = table.remove(State.player.queue, 1)
- else
- State.player.now_playing = nil
- State.player.playing = false
- State.net.is_loading = false
- end
- State.player.playing_id = nil
- end
- os.queueEvent("redraw_screen")
- State.player.handle.close()
- State.player.needs_next_chunk = 0
- break
- else
- if start_chunk then
- chunk, start_chunk = start_chunk .. chunk, nil
- size = size + 4
- end
- buffer = decoder(chunk)
- local fn = {}
- for i, speaker in ipairs(speakers) do
- fn[i] = function()
- local name = peripheral.getName(speaker)
- while not speaker.playAudio(buffer, State.player.volume) do
- parallel.waitForAny(
- function() repeat until select(2, os.pullEvent("speaker_audio_empty")) == name end,
- function() os.pullEvent("playback_stopped") end
- )
- if not State.player.playing or State.player.playing_id ~= current_id then return end
- end
- end
- end
- local ok = pcall(parallel.waitForAll, table.unpack(fn))
- if not ok then
- State.player.needs_next_chunk = 2
- State.net.is_error = true
- break
- end
- if not State.player.playing or State.player.playing_id ~= current_id 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 _, url, handle = os.pullEvent("http_success")
- if url == State.net.last_search_url then
- State.net.search_results = textutils.unserialiseJSON(handle.readAll())
- os.queueEvent("redraw_screen")
- elseif url == State.net.last_download_url then
- State.net.is_loading = false
- State.player.handle = handle
- start_chunk = handle.read(4)
- size = 16 * 1024 - 4
- State.player.status = 1
- os.queueEvent("redraw_screen")
- os.queueEvent("audio_update")
- end
- end,
- function()
- local _, url = os.pullEvent("http_failure")
- if url == State.net.last_search_url then
- State.net.search_error = true
- os.queueEvent("redraw_screen")
- elseif url == State.net.last_download_url then
- State.net.is_loading = false
- State.net.is_error = true
- State.player.playing = false
- State.player.playing_id = nil
- os.queueEvent("redraw_screen")
- os.queueEvent("audio_update")
- end
- end
- )
- end
- end
- parallel.waitForAny(uiLoop, audioLoop, httpLoop)
Advertisement
Add Comment
Please, Sign In to add comment