--[[- Lua library for accessing [YouCub's API](https://commandcracker.github.io/YouCube/) @module youcubeapi ]] --[[ youcubeapi.lua _ _ ____ _ _ ____ _ _ ___ ____ ____ ___ _ \_/ | | | | | | | |__] |___ |__| |__] | | |__| |__| |___ |__| |__] |___ | | | | ]] --[[- "wrapper" for accessing [YouCub's API](https://commandcracker.github.io/YouCube/) @type API @usage Example: local youcubeapi = require("youcubeapi") local api = youcubeapi.API.new() api:detect_bestest_server() api:request_media(url) local data = api.websocket.receive() ]] local API = {} --- Create's a new API instance. -- @param websocket [Websocket](https://tweaked.cc/module/http.html#ty:Websocket) The websocket. -- @treturn API instance function API.new(websocket) return setmetatable({ websocket = websocket, }, { __index = API }) end -- Look at the [Documentation](https://commandcracker.github.io/YouCube/) for moor information -- Contact the server owner on Discord, when the server is down local servers = { "ws://127.0.0.1:5000", -- Your server! "wss://us-ky.youcube.knijn.one", -- By EmmaKnijn "wss://youcube.knijn.one", -- By EmmaKnijn "wss://youcube.onrender.com", -- By Commandcracker#8528 } if settings then local server = settings.get("youcube.server") if server then table.insert(servers, 1, server) end end local function websocket_with_timeout(_url, _headers, _timeout) if http.websocketAsync then local websocket, websocket_error = http.websocketAsync(_url, _headers) if not websocket then return false, websocket_error end local timerID = os.startTimer(_timeout) while true do local event, param1, param2 = os.pullEvent() -- TODO: Close web-socket when the connection succeeds after the timeout if event == "websocket_success" and param1 == _url then return param2 elseif event == "websocket_failure" and param1 == _url then return false, param2 elseif event == "timer" and param1 == timerID then return false, "Timeout" end end end -- use websocket without timeout -- when the CC version dos not support websocketAsync return http.websocket(_url, _headers) end --- Connects to a YouCub Server function API:detect_bestest_server(_server, _verbose) if _server then table.insert(servers, 1, _server) end for i = 1, #servers do local server = servers[i] local ok, err = http.checkURL(server:gsub("^ws://", "http://"):gsub("^wss://", "https://")) if ok then if _verbose then print("Trying to connect to:", server) end local websocket, websocket_error = websocket_with_timeout(server, nil, 5) if websocket ~= false then term.write("Using the YouCube server: ") term.setTextColor(colors.blue) print(server) term.setTextColor(colors.white) self.websocket = websocket break elseif i == #servers then error(websocket_error) elseif _verbose then print(websocket_error) end elseif i == #servers then error(err) elseif _verbose then print(err) end end end --- Receive data from The YouCub Server -- @tparam string filter action filter -- @treturn table retval data function API:receive(filter) local status, retval = pcall(self.websocket.receive) if not status then error("Lost connection to server\n" .. retval) end if retval == nil then error("Received empty message or max message size exceeded") end local data, err = textutils.unserialiseJSON(retval) if data == nil then error("Failed to parse message\n" .. err) end if filter then --if type(filter) == "table" then -- if not filter[data.action] then -- return self:receive(filter) -- end --else if data.action ~= filter then return self:receive(filter) end end return data end --- Send data to The YouCub Server -- @tparam table data data to send function API:send(data) local status, retval = pcall(self.websocket.send, textutils.serialiseJSON(data)) if not status then error("Lost connection to server\n" .. retval) end end --[[- [Base64](https://wikipedia.org/wiki/Base64) functions @type Base64 ]] local Base64 = {} local b64str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" -- based on https://github.com/MCJack123/sanjuuni/blob/c64f8725a9f24dec656819923457717dfb964515/raw-player.lua --- Decode base64 string -- @tparam string str base64 string -- @treturn string string decoded string function Base64.decode(str) local retval = "" for s in str:gmatch("....") do if s:sub(3, 4) == "==" then retval = retval .. string.char( bit32.bor( bit32.lshift(b64str:find(s:sub(1, 1)) - 1, 2), bit32.rshift(b64str:find(s:sub(2, 2)) - 1, 4) ) ) elseif s:sub(4, 4) == "=" then local n = (b64str:find(s:sub(1, 1)) - 1) * 4096 + (b64str:find(s:sub(2, 2)) - 1) * 64 + (b64str:find(s:sub(3, 3)) - 1) retval = retval .. string.char(bit32.extract(n, 10, 8)) .. string.char(bit32.extract(n, 2, 8)) else local n = (b64str:find(s:sub(1, 1)) - 1) * 262144 + (b64str:find(s:sub(2, 2)) - 1) * 4096 + (b64str:find(s:sub(3, 3)) - 1) * 64 + (b64str:find(s:sub(4, 4)) - 1) retval = retval .. string.char(bit32.extract(n, 16, 8)) .. string.char(bit32.extract(n, 8, 8)) .. string.char(bit32.extract(n, 0, 8)) end end return retval end --- Request a `16 * 1024` bit chunk -- @tparam number chunkindex The chunkindex -- @tparam string id Media id -- @treturn bytes chunk `16 * 1024` bit chunk function API:get_chunk(chunkindex, id) self:send({ ["action"] = "get_chunk", ["chunkindex"] = chunkindex, ["id"] = id, }) return Base64.decode(self:receive("chunk").chunk) end --- Get 32vid -- @tparam number line The line to return -- @tparam string id Media id -- @tparam number width Video width -- @tparam number height Video height -- @treturn string line one line of the given 32vid function API:get_vid(tracker, id, width, height) self:send({ ["action"] = "get_vid", ["tracker"] = tracker, ["id"] = id, ["width"] = width * 2, ["height"] = height * 3, }) return self:receive("vid") end --- Request media -- @tparam string url Url or Search Term --@treturn table json response function API:request_media(url, width, height) local request = { ["action"] = "request_media", ["url"] = url, } if width and height then request.width = width * 2 request.height = height * 3 end self:send(request) --return self:receive({ ["media"] = true, ["status"] = true }) end --- Handshake - get Server capabilities and version --@treturn table json response function API:handshake() self:send({ ["action"] = "handshake", }) return self:receive("handshake") end --[[- Abstraction for Audio Devices @type AudioDevice ]] local AudioDevice = {} --- Create's a new AudioDevice instance. -- @tparam table object Base values -- @treturn AudioDevice instance function AudioDevice.new(object) -- @type AudioDevice local self = object or {} function self:validate() end function self:setLabel(lable) end function self:write(chunk) end function self:play() end function self:reset() end function self:setVolume(volume) end return self end --[[- @{AudioDevice} from a Speaker @type Speaker @usage Example: local youcubeapi = require("youcubeapi") local speaker = peripheral.find("speaker") local audiodevice = youcubeapi.Speaker.new(speaker) ]] local Speaker = {} local decoder local status, dfpwm = pcall(require, "cc.audio.dfpwm") if status then decoder = dfpwm.make_decoder() end --- Create's a new Tape instance. -- @tparam speaker speaker The speaker -- @treturn AudioDevice|Speaker instance function Speaker.new(speaker) local self = AudioDevice.new({ speaker = speaker }) function self:validate() if not decoder then return "This ComputerCraft version dos not support DFPWM" end end function self:setVolume(volume) self.volume = volume end function self:write(chunk) local buffer = decoder(chunk) while not self.speaker.playAudio(buffer, self.volume) do os.pullEvent("speaker_audio_empty") end end return self end --[[- @{AudioDevice} from a [Computronics tape_drive](https://wiki.vexatos.com/wiki:computronics:tape) @type Tape @usage Example: local youcubeapi = require("youcubeapi") local tape_drive = peripheral.find("tape_drive") local audiodevice = youcubeapi.Tape.new(tape_drive) ]] local Tape = {} --- Create's a new Tape instance. -- @tparam tape tape The tape_drive -- @treturn AudioDevice|Tape instance function Tape.new(tape) local self = AudioDevice.new({ tape = tape }) function self:validate() if not self.tape.isReady() then return "You need to insert a tape" end end function self:setVolume(volume) if volume then self.tape.setVolume(volume) end end function self:play(chunk) self.tape.seek(-self.tape.getSize()) self.tape.play() end function self:write(chunk) self.tape.write(chunk) end function self:setLabel(lable) self.tape.setLabel(lable) end function self:reset() -- based on https://github.com/Vexatos/Computronics/blob/b0ade53cab10529dbe91ebabfa882d1b4b21fa90/src/main/resources/assets/computronics/lua/peripheral/tape_drive/programs/tape_drive/tape#L109-L123 local size = self.tape.getSize() self.tape.stop() self.tape.seek(-size) self.tape.stop() self.tape.seek(-90000) local s = string.rep(string.char(170), 8192) for i = 1, size + 8191, 8192 do self.tape.write(s) end self.tape.seek(-size) self.tape.seek(-90000) end return self end --[[- Abstract object for filling a @{Buffer} @type Filler ]] local Filler = {} --- Create's a new Filler instance. -- @treturn Filler instance function Filler.new() local self = {} function self:next() end return self end --[[- @{Filler} for Audio @type AudioFiller ]] local AudioFiller = {} --- Create's a new AudioFiller instance. -- @tparam API youcubeapi API object -- @tparam string id Media id -- @treturn AudioFiller|Filler instance function AudioFiller.new(youcubeapi, id) local self = { id = id, chunkindex = 0, youcubeapi = youcubeapi, } function self:next() local response = self.youcubeapi:get_chunk(self.chunkindex, self.id) self.chunkindex = self.chunkindex + 1 return response end return self end --[[- @{Filler} for Video @type VideoFiller ]] local VideoFiller = {} --- Create's a new VideoFiller instance. -- @tparam API youcubeapi API object -- @tparam string id Media id -- @tparam number width Video width -- @tparam number height Video height -- @treturn VideoFiller|Filler instance function VideoFiller.new(youcubeapi, id, width, height) local self = { id = id, width = width, height = height, tracker = 0, youcubeapi = youcubeapi, } function self:next() local response = self.youcubeapi:get_vid(self.tracker, self.id, self.width, self.height) for i = 1, #response.lines do self.tracker = self.tracker + #response.lines[i] + 1 end return response.lines end return self end --[[- Buffers Data @type Buffer ]] local Buffer = {} --- Create's a new Buffer instance. -- @tparam Filler filler filler instance -- @tparam number size buffer limit -- @treturn Buffer instance function Buffer.new(filler, size) local self = { filler = filler, size = size, } self.buffer = {} function self:next() while #self.buffer == 0 do os.pullEvent() end -- Wait until next is available local next = self.buffer[1] table.remove(self.buffer, 1) return next end function self:fill() if #self.buffer < self.size then local next = filler:next() if type(next) == "table" then for i = 1, #next do self.buffer[#self.buffer + 1] = next[i] end else self.buffer[#self.buffer + 1] = next end return true end return false end return self end local currnt_palette = {} for i = 0, 15 do local r, g, b = term.getPaletteColour(2 ^ i) currnt_palette[i] = { r, g, b } end local function reset_term() for i = 0, 15 do term.setPaletteColor(2 ^ i, currnt_palette[i][1], currnt_palette[i][2], currnt_palette[i][3]) end term.setBackgroundColor(colors.black) term.setTextColor(colors.white) term.clear() term.setCursorPos(1, 1) end --[[- Create's a new Buffer instance. Based on [sanjuuni/raw-player.lua](https://github.com/MCJack123/sanjuuni/blob/c64f8725a9f24dec656819923457717dfb964515/raw-player.lua) and [sanjuuni/websocket-player.lua](https://github.com/MCJack123/sanjuuni/blob/30dcabb4b56f1eb32c88e1bce384b0898367ebda/websocket-player.lua) @tparam Buffer buffer filled with frames ]] local function play_vid(buffer, force_fps, string_unpack) if not string_unpack then string_unpack = string.unpack end local Fwidth, Fheight = term.getSize() local tracker = 0 if buffer:next() ~= "32Vid 1.1" then error("Unsupported file") end local fps = tonumber(buffer:next()) if force_fps then fps = force_fps end -- Adjust buffer size buffer.size = math.ceil(fps) * 2 local first, second = buffer:next(), buffer:next() if second == "" or second == nil then fps = 0 end term.clear() local start = os.epoch("utc") local frame_count = 0 while true do frame_count = frame_count + 1 local frame if first then frame, first = first, nil elseif second then frame, second = second, nil else frame = buffer:next() end if frame == "" or frame == nil then break end local mode = frame:match("^!CP([CD])") if not mode then error("Invalid file") end local b64data if mode == "C" then local len = tonumber(frame:sub(5, 8), 16) b64data = frame:sub(9, len + 8) else local len = tonumber(frame:sub(5, 16), 16) b64data = frame:sub(17, len + 16) end local data = Base64.decode(b64data) -- TODO: maybe verify checksums? assert(data:sub(1, 4) == "\0\0\0\0" and data:sub(9, 16) == "\0\0\0\0\0\0\0\0", "Invalid file") local width, height = string_unpack("HH", data, 5) local c, n, pos = string_unpack("c1B", data, 17) local text = {} for y = 1, height do text[y] = "" for x = 1, width do text[y] = text[y] .. c n = n - 1 if n == 0 then c, n, pos = string_unpack("c1B", data, pos) end end end c = c:byte() for y = 1, height do local fg, bg = "", "" for x = 1, width do fg, bg = fg .. ("%x"):format(bit32.band(c, 0x0F)), bg .. ("%x"):format(bit32.rshift(c, 4)) n = n - 1 if n == 0 then c, n, pos = string_unpack("BB", data, pos) end end term.setCursorPos(1, y) term.blit(text[y], fg, bg) end pos = pos - 2 local r, g, b for i = 0, 15 do r, g, b, pos = string_unpack("BBB", data, pos) term.setPaletteColor(2 ^ i, r / 255, g / 255, b / 255) end if fps == 0 then read() break else while os.epoch("utc") < start + (frame_count + 1) / fps * 1000 do sleep(1 / fps) end end end reset_term() end return { --- "Metadata" - [YouCube API](https://commandcracker.github.io/YouCube/) Version _API_VERSION = "0.0.0-poc.1.0.0", --- "Metadata" - Library Version _VERSION = "0.0.0-poc.1.4.2", --- "Metadata" - Description _DESCRIPTION = "Library for accessing YouCub's API", --- "Metadata" - Homepage / Url _URL = "https://github.com/Commandcracker/YouCube", --- "Metadata" - License _LICENSE = "GPL-3.0", API = API, AudioDevice = AudioDevice, Speaker = Speaker, Tape = Tape, Base64 = Base64, Filler = Filler, AudioFiller = AudioFiller, VideoFiller = VideoFiller, Buffer = Buffer, play_vid = play_vid, reset_term = reset_term, }