:3
This commit is contained in:
parent
491112768c
commit
68ec37f994
66 changed files with 6591 additions and 10096 deletions
637
computer/28/lib/youcubeapi.lua
Normal file
637
computer/28/lib/youcubeapi.lua
Normal 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,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue