Luke_Arwolf

Musica Computer

Apr 10th, 2026 (edited)
24
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 19.22 KB | None | 0 0
  1. local API_BASE_URL = "https://ipod-2to6magyna-uc.a.run.app/"
  2. local VERSION = "2.1"
  3. local width, height = term.getSize()
  4.  
  5. local State = {
  6. ui = {
  7. tab = 1,
  8. waiting_for_input = false,
  9. in_search_result = false,
  10. clicked_result = nil,
  11. },
  12. net = {
  13. last_search = nil,
  14. last_search_url = nil,
  15. search_results = nil,
  16. search_error = false,
  17. last_download_url = nil,
  18. is_loading = false,
  19. is_error = false
  20. },
  21. player = {
  22. playing = false,
  23. playing_id = nil,
  24. status = 0,
  25. queue = {},
  26. now_playing = nil,
  27. looping = 0,
  28. volume = 1.5,
  29. handle = nil,
  30. needs_next_chunk = 0
  31. }
  32. }
  33.  
  34. local start_chunk, pcm, size, buffer
  35. local decoder = require("cc.audio.dfpwm").make_decoder()
  36. local speakers = { peripheral.find("speaker") }
  37.  
  38. if #speakers == 0 then
  39. error("No speakers attached.", 0)
  40. end
  41.  
  42. local accent_color = colors.lightBlue
  43. local bg_color = colors.black
  44. local panel_color = colors.gray
  45. local text_color = colors.white
  46. local subtext_color = colors.lightGray
  47.  
  48. local function stopPlayback()
  49. State.player.playing = false
  50. for _, speaker in ipairs(speakers) do
  51. speaker.stop()
  52. end
  53. os.queueEvent("playback_stopped")
  54. end
  55.  
  56. local function playSong(songData)
  57. stopPlayback()
  58. State.player.playing = true
  59. State.net.is_error = false
  60. State.player.playing_id = nil
  61. State.player.now_playing = songData
  62. os.queueEvent("audio_update")
  63. end
  64.  
  65. local function addToQueue(item, playNext)
  66. local itemsToAdd = (item.type == "playlist") and item.playlist_items or {item}
  67. if playNext then
  68. for i = #itemsToAdd, 1, -1 do
  69. table.insert(State.player.queue, 1, itemsToAdd[i])
  70. end
  71. else
  72. for i = 1, #itemsToAdd do
  73. table.insert(State.player.queue, itemsToAdd[i])
  74. end
  75. end
  76. os.queueEvent("audio_update")
  77. end
  78.  
  79. local function drawButton(x, y, text, active)
  80. term.setCursorPos(x, y)
  81. if active then
  82. term.setTextColor(bg_color)
  83. term.setBackgroundColor(accent_color)
  84. else
  85. term.setTextColor(text_color)
  86. term.setBackgroundColor(panel_color)
  87. end
  88. term.write(text)
  89. term.setBackgroundColor(bg_color)
  90. end
  91.  
  92. local function drawTabs()
  93. term.setCursorPos(1, 1)
  94. term.setBackgroundColor(panel_color)
  95. term.clearLine()
  96. local tabs = {" Now Playing ", " Search "}
  97. for i = 1, #tabs do
  98. local isActive = (State.ui.tab == i)
  99. local xPos = (math.floor((width / #tabs) * (i - 0.5))) - math.ceil(#tabs[i] / 2) + 1
  100. drawButton(xPos, 1, tabs[i], isActive)
  101. end
  102. end
  103.  
  104. local function drawNowPlaying()
  105. term.setBackgroundColor(bg_color)
  106. if State.player.now_playing then
  107. term.setTextColor(accent_color)
  108. term.setCursorPos(2, 3)
  109. term.write(State.player.now_playing.name)
  110. term.setTextColor(text_color)
  111. term.setCursorPos(2, 4)
  112. term.write(State.player.now_playing.artist)
  113. else
  114. term.setTextColor(subtext_color)
  115. term.setCursorPos(2, 3)
  116. term.write("Not playing")
  117. end
  118.  
  119. term.setCursorPos(2, 5)
  120. if State.net.is_loading then
  121. term.setTextColor(colors.yellow)
  122. term.write("Loading stream...")
  123. elseif State.net.is_error then
  124. term.setTextColor(colors.red)
  125. term.write("Network error")
  126. else
  127. term.clearLine()
  128. end
  129.  
  130. local hasMedia = (State.player.now_playing ~= nil or #State.player.queue > 0)
  131. drawButton(2, 6, State.player.playing and " Stop " or " Play ", State.player.playing)
  132.  
  133. term.setCursorPos(2 + 7, 6)
  134. term.setTextColor(hasMedia and text_color or subtext_color)
  135. term.setBackgroundColor(panel_color)
  136. term.write(" Skip ")
  137.  
  138. local loopText = State.player.looping == 0 and " Loop Off " or (State.player.looping == 1 and " Loop Queue " or " Loop Song ")
  139. drawButton(2 + 7 + 7, 6, loopText, State.player.looping ~= 0)
  140.  
  141. term.setCursorPos(2, 8)
  142. paintutils.drawBox(2, 8, 25, 8, panel_color)
  143. local volWidth = math.floor(24 * (State.player.volume / 3) + 0.5) - 1
  144. if volWidth >= 0 then
  145. paintutils.drawBox(2, 8, 2 + volWidth, 8, accent_color)
  146. end
  147.  
  148. local volTextPos = (State.player.volume < 0.6) and (2 + volWidth + 2) or (2 + volWidth - 3 - (State.player.volume == 3 and 1 or 0))
  149. term.setCursorPos(volTextPos, 8)
  150. term.setBackgroundColor(State.player.volume < 0.6 and panel_color or accent_color)
  151. term.setTextColor(State.player.volume < 0.6 and text_color or bg_color)
  152. term.write(math.floor(100 * (State.player.volume / 3) + 0.5) .. "%")
  153.  
  154. if #State.player.queue > 0 then
  155. term.setBackgroundColor(bg_color)
  156. term.setTextColor(subtext_color)
  157. term.setCursorPos(2, 10)
  158. term.write("Up Next:")
  159. for i = 1, math.min(#State.player.queue, 4) do
  160. term.setTextColor(text_color)
  161. term.setCursorPos(2, 10 + i * 2)
  162. term.write("- " .. string.sub(State.player.queue[i].name, 1, width - 4))
  163. term.setTextColor(subtext_color)
  164. term.setCursorPos(4, 11 + i * 2)
  165. term.write(string.sub(State.player.queue[i].artist, 1, width - 6))
  166. end
  167. end
  168. end
  169.  
  170. local function drawSearch()
  171. paintutils.drawFilledBox(2, 3, width - 1, 5, panel_color)
  172. term.setBackgroundColor(panel_color)
  173. term.setTextColor(accent_color)
  174. term.setCursorPos(3, 4)
  175. if State.net.last_search then
  176. term.write(string.sub(State.net.last_search, 1, width - 4))
  177. else
  178. term.write("Search or paste URL...")
  179. end
  180.  
  181. term.setBackgroundColor(bg_color)
  182. if State.ui.in_search_result then
  183. term.clear()
  184. local result = State.net.search_results[State.ui.clicked_result]
  185. term.setCursorPos(2, 2)
  186. term.setTextColor(accent_color)
  187. term.write(string.sub(result.name, 1, width - 2))
  188. term.setCursorPos(2, 3)
  189. term.setTextColor(text_color)
  190. term.write(string.sub(result.artist, 1, width - 2))
  191.  
  192. local options = {"Play now", "Play next", "Add to queue", "Cancel"}
  193. local yPos = {6, 8, 10, 13}
  194. for i, opt in ipairs(options) do
  195. term.setCursorPos(2, yPos[i])
  196. term.setBackgroundColor(panel_color)
  197. term.setTextColor(text_color)
  198. term.clearLine()
  199. term.write(" " .. opt .. " ")
  200. end
  201. else
  202. if State.net.search_results then
  203. for i = 1, #State.net.search_results do
  204. term.setTextColor(text_color)
  205. term.setCursorPos(2, 7 + (i - 1) * 2)
  206. term.write(string.sub(State.net.search_results[i].name, 1, width - 2))
  207. term.setTextColor(subtext_color)
  208. term.setCursorPos(2, 8 + (i - 1) * 2)
  209. term.write(string.sub(State.net.search_results[i].artist, 1, width - 2))
  210. end
  211. else
  212. term.setCursorPos(2, 7)
  213. if State.net.search_error then
  214. term.setTextColor(colors.red)
  215. term.write("Network error")
  216. elseif State.net.last_search_url then
  217. term.setTextColor(accent_color)
  218. term.write("Searching...")
  219. else
  220. term.setCursorPos(2, 7)
  221. term.setTextColor(subtext_color)
  222. term.write("Waiting for search query...")
  223. end
  224. end
  225. end
  226. end
  227.  
  228. function redrawScreen()
  229. if State.ui.waiting_for_input then return end
  230. term.setCursorBlink(false)
  231. term.setBackgroundColor(bg_color)
  232. term.clear()
  233. drawTabs()
  234. if State.ui.tab == 1 then
  235. drawNowPlaying()
  236. elseif State.ui.tab == 2 then
  237. drawSearch()
  238. end
  239.  
  240. local watermark = "Criado por Luke Arwolf"
  241. if math.floor(os.clock() / 10) % 2 == 0 then
  242. watermark = "Criado por Luke Arwolf"
  243. term.setTextColor(accent_color)
  244. else
  245. term.setTextColor(subtext_color)
  246. end
  247. term.setCursorPos(width - #watermark + 1, height)
  248. term.setBackgroundColor(bg_color)
  249. term.write(watermark)
  250. end
  251.  
  252. local function handleSearchInputClick(x, y)
  253. if y >= 3 and y <= 5 and x >= 1 and x <= width - 1 then
  254. paintutils.drawFilledBox(2, 3, width - 1, 5, text_color)
  255. State.ui.waiting_for_input = true
  256. return true
  257. end
  258. return false
  259. end
  260.  
  261. local function handleSearchResultClick(x, y)
  262. if not State.net.search_results then return false end
  263. for i = 1, #State.net.search_results do
  264. if y == 7 + (i - 1) * 2 or y == 8 + (i - 1) * 2 then
  265. drawButton(2, 7 + (i - 1) * 2, string.sub(State.net.search_results[i].name, 1, width - 2), true)
  266. sleep(0.1)
  267. State.ui.in_search_result = true
  268. State.ui.clicked_result = i
  269. redrawScreen()
  270. return true
  271. end
  272. end
  273. return false
  274. end
  275.  
  276. local function handleSongMenuClick(y)
  277. local result = State.net.search_results[State.ui.clicked_result]
  278. if y == 6 then
  279. if result.type == "playlist" then
  280. State.player.queue = {}
  281. if #result.playlist_items > 1 then
  282. for i = 2, #result.playlist_items do table.insert(State.player.queue, result.playlist_items[i]) end
  283. end
  284. playSong(result.playlist_items[1])
  285. else
  286. playSong(result)
  287. end
  288. elseif y == 8 then
  289. addToQueue(result, true)
  290. elseif y == 10 then
  291. addToQueue(result, false)
  292. elseif y ~= 13 then
  293. return
  294. end
  295. sleep(0.1)
  296. State.ui.in_search_result = false
  297. redrawScreen()
  298. end
  299.  
  300. local function handleControlsClick(x, y)
  301. if y == 6 then
  302. if x >= 2 and x < 8 then
  303. if State.player.playing then
  304. stopPlayback()
  305. State.player.playing_id = nil
  306. State.net.is_loading = false
  307. elseif State.player.now_playing or #State.player.queue > 0 then
  308. if not State.player.now_playing then
  309. State.player.now_playing = table.remove(State.player.queue, 1)
  310. end
  311. State.player.playing = true
  312. State.player.playing_id = nil
  313. os.queueEvent("audio_update")
  314. end
  315. elseif x >= 9 and x < 15 and (State.player.now_playing or #State.player.queue > 0) then
  316. stopPlayback()
  317. if #State.player.queue > 0 then
  318. if State.player.looping == 1 then table.insert(State.player.queue, State.player.now_playing) end
  319. State.player.now_playing = table.remove(State.player.queue, 1)
  320. State.player.playing_id = nil
  321. State.player.playing = true
  322. else
  323. State.player.now_playing = nil
  324. State.player.playing_id = nil
  325. State.net.is_loading = false
  326. end
  327. os.queueEvent("audio_update")
  328. elseif x >= 16 and x < 28 then
  329. State.player.looping = (State.player.looping + 1) % 3
  330. end
  331. elseif y >= 7 and y <= 9 then
  332. if x >= 1 and x < 26 then
  333. State.player.volume = (x - 1) / 24 * 3
  334. end
  335. end
  336. redrawScreen()
  337. end
  338.  
  339. function uiLoop()
  340. local timer = os.startTimer(1)
  341. redrawScreen()
  342. while true do
  343. if State.ui.waiting_for_input then
  344. parallel.waitForAny(
  345. function()
  346. term.setCursorPos(3, 4)
  347. term.setBackgroundColor(text_color)
  348. term.setTextColor(bg_color)
  349. local input = read()
  350. if string.len(input) > 0 then
  351. State.net.last_search = input
  352. State.net.last_search_url = API_BASE_URL .. "?v=" .. VERSION .. "&search=" .. textutils.urlEncode(input)
  353. http.request(State.net.last_search_url)
  354. State.net.search_results = nil
  355. State.net.search_error = false
  356. end
  357. State.ui.waiting_for_input = false
  358. os.queueEvent("redraw_screen")
  359. end,
  360. function()
  361. while State.ui.waiting_for_input do
  362. local _, _, mx, my = os.pullEvent("mouse_click")
  363. if my < 3 or my > 5 or mx < 2 or mx > width - 1 then
  364. State.ui.waiting_for_input = false
  365. os.queueEvent("redraw_screen")
  366. break
  367. end
  368. end
  369. end
  370. )
  371. else
  372. local event, p1, p2, p3 = os.pullEvent()
  373. if event == "mouse_click" and p1 == 1 then
  374. local x, y = p2, p3
  375. if not State.ui.in_search_result and y == 1 then
  376. State.ui.tab = (x < width / 2) and 1 or 2
  377. redrawScreen()
  378. elseif State.ui.tab == 2 then
  379. if not State.ui.in_search_result then
  380. if not handleSearchInputClick(x, y) then handleSearchResultClick(x, y) end
  381. else
  382. handleSongMenuClick(y)
  383. end
  384. elseif State.ui.tab == 1 then
  385. handleControlsClick(x, y)
  386. end
  387. elseif event == "mouse_drag" and p1 == 1 and State.ui.tab == 1 and not State.ui.in_search_result then
  388. if p3 >= 7 and p3 <= 9 and p2 >= 1 and p2 < 26 then
  389. State.player.volume = (p2 - 1) / 24 * 3
  390. redrawScreen()
  391. end
  392. elseif event == "timer" and p1 == timer then
  393. redrawScreen()
  394. timer = os.startTimer(1)
  395. elseif event == "redraw_screen" then
  396. redrawScreen()
  397. end
  398. end
  399. end
  400. end
  401.  
  402. function audioLoop()
  403. while true do
  404. if State.player.playing and State.player.now_playing then
  405. local current_id = State.player.now_playing.id
  406. if State.player.playing_id ~= current_id then
  407. State.player.playing_id = current_id
  408. State.net.last_download_url = API_BASE_URL .. "?v=" .. VERSION .. "&id=" .. textutils.urlEncode(current_id)
  409. State.player.status = 0
  410. State.player.needs_next_chunk = 1
  411. http.request({url = State.net.last_download_url, binary = true})
  412. State.net.is_loading = true
  413. os.queueEvent("redraw_screen")
  414. os.queueEvent("audio_update")
  415. elseif State.player.status == 1 and State.player.needs_next_chunk == 1 then
  416. while true do
  417. local chunk = State.player.handle.read(size)
  418. if not chunk then
  419. if State.player.looping == 2 or (State.player.looping == 1 and #State.player.queue == 0) then
  420. State.player.playing_id = nil
  421. elseif State.player.looping == 1 and #State.player.queue > 0 then
  422. table.insert(State.player.queue, State.player.now_playing)
  423. State.player.now_playing = table.remove(State.player.queue, 1)
  424. State.player.playing_id = nil
  425. else
  426. if #State.player.queue > 0 then
  427. State.player.now_playing = table.remove(State.player.queue, 1)
  428. else
  429. State.player.now_playing = nil
  430. State.player.playing = false
  431. State.net.is_loading = false
  432. end
  433. State.player.playing_id = nil
  434. end
  435. os.queueEvent("redraw_screen")
  436. State.player.handle.close()
  437. State.player.needs_next_chunk = 0
  438. break
  439. else
  440. if start_chunk then
  441. chunk, start_chunk = start_chunk .. chunk, nil
  442. size = size + 4
  443. end
  444. buffer = decoder(chunk)
  445. local fn = {}
  446. for i, speaker in ipairs(speakers) do
  447. fn[i] = function()
  448. local name = peripheral.getName(speaker)
  449. while not speaker.playAudio(buffer, State.player.volume) do
  450. parallel.waitForAny(
  451. function() repeat until select(2, os.pullEvent("speaker_audio_empty")) == name end,
  452. function() os.pullEvent("playback_stopped") end
  453. )
  454. if not State.player.playing or State.player.playing_id ~= current_id then return end
  455. end
  456. end
  457. end
  458. local ok = pcall(parallel.waitForAll, table.unpack(fn))
  459. if not ok then
  460. State.player.needs_next_chunk = 2
  461. State.net.is_error = true
  462. break
  463. end
  464. if not State.player.playing or State.player.playing_id ~= current_id then break end
  465. end
  466. end
  467. os.queueEvent("audio_update")
  468. end
  469. end
  470. os.pullEvent("audio_update")
  471. end
  472. end
  473.  
  474. function httpLoop()
  475. while true do
  476. parallel.waitForAny(
  477. function()
  478. local _, url, handle = os.pullEvent("http_success")
  479. if url == State.net.last_search_url then
  480. State.net.search_results = textutils.unserialiseJSON(handle.readAll())
  481. os.queueEvent("redraw_screen")
  482. elseif url == State.net.last_download_url then
  483. State.net.is_loading = false
  484. State.player.handle = handle
  485. start_chunk = handle.read(4)
  486. size = 16 * 1024 - 4
  487. State.player.status = 1
  488. os.queueEvent("redraw_screen")
  489. os.queueEvent("audio_update")
  490. end
  491. end,
  492. function()
  493. local _, url = os.pullEvent("http_failure")
  494. if url == State.net.last_search_url then
  495. State.net.search_error = true
  496. os.queueEvent("redraw_screen")
  497. elseif url == State.net.last_download_url then
  498. State.net.is_loading = false
  499. State.net.is_error = true
  500. State.player.playing = false
  501. State.player.playing_id = nil
  502. os.queueEvent("redraw_screen")
  503. os.queueEvent("audio_update")
  504. end
  505. end
  506. )
  507. end
  508. end
  509.  
  510. parallel.waitForAny(uiLoop, audioLoop, httpLoop)
Advertisement
Add Comment
Please, Sign In to add comment