This commit is contained in:
Crispy 2025-07-03 01:23:46 +02:00
parent 491112768c
commit 68ec37f994
66 changed files with 6591 additions and 10096 deletions

View file

@ -0,0 +1,637 @@
--[[- 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,
}