447 lines
No EOL
8.9 KiB
Lua
447 lines
No EOL
8.9 KiB
Lua
local base_url = 'http://localhost:8899'
|
|
|
|
-- Maximum amount of time an entire request may take
|
|
local request_timeout = 3
|
|
|
|
-- Maximum time a single socket operation may block
|
|
local socket_timeout = 0.01
|
|
|
|
-- How long to give MA3 back control between operations
|
|
local yield_time = 0.01
|
|
|
|
local socket = require("socket")
|
|
local json = require("json")
|
|
|
|
local columns = {}
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- UI helpers
|
|
----------------------------------------------------------------
|
|
|
|
local function show_message(title, message)
|
|
MessageBox({
|
|
title = title,
|
|
message = message,
|
|
commands = {
|
|
{ value = 1, name = "Ok" },
|
|
},
|
|
})
|
|
end
|
|
|
|
|
|
local function show_error(message)
|
|
ErrEcho("Resolume: " .. tostring(message))
|
|
show_message("Resolume Error", tostring(message))
|
|
end
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- Utility
|
|
----------------------------------------------------------------
|
|
|
|
local function yield()
|
|
coroutine.yield(yield_time)
|
|
end
|
|
|
|
|
|
local function parse_url(url)
|
|
local host, port, path
|
|
|
|
host, port, path = url:match(
|
|
"^http://([^:/]+):(%d+)(/.*)$"
|
|
)
|
|
|
|
if not host then
|
|
host, path = url:match(
|
|
"^http://([^/]+)(/.*)$"
|
|
)
|
|
|
|
if host then
|
|
port = 80
|
|
end
|
|
end
|
|
|
|
if not host then
|
|
host = url:match("^http://([^/]+)$")
|
|
|
|
if host then
|
|
port = 80
|
|
path = "/"
|
|
end
|
|
end
|
|
|
|
if not host then
|
|
return nil
|
|
end
|
|
|
|
return host, tonumber(port), path
|
|
end
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- HTTP
|
|
----------------------------------------------------------------
|
|
|
|
local function http_request(url, method, body)
|
|
|
|
local host, port, path = parse_url(url)
|
|
|
|
if not host then
|
|
show_error("Invalid URL: " .. tostring(url))
|
|
return nil
|
|
end
|
|
|
|
local start_time = os.clock()
|
|
|
|
------------------------------------------------------------
|
|
-- Create socket
|
|
------------------------------------------------------------
|
|
|
|
local tcp, err = socket.tcp()
|
|
|
|
if not tcp then
|
|
show_error("Could not create socket: " .. tostring(err))
|
|
return nil
|
|
end
|
|
|
|
-- Each individual socket operation can block for at most
|
|
-- socket_timeout seconds.
|
|
tcp:settimeout(socket_timeout)
|
|
|
|
------------------------------------------------------------
|
|
-- Connect
|
|
------------------------------------------------------------
|
|
|
|
local connected = false
|
|
|
|
while not connected do
|
|
|
|
local ok, connect_err = tcp:connect(host, port)
|
|
|
|
if ok then
|
|
connected = true
|
|
|
|
elseif connect_err == "timeout" then
|
|
-- Connection is still in progress.
|
|
-- Give MA3 some time before trying again.
|
|
yield()
|
|
|
|
else
|
|
tcp:close()
|
|
|
|
show_error(
|
|
"Connection failed: " ..
|
|
tostring(connect_err)
|
|
)
|
|
|
|
return nil
|
|
end
|
|
|
|
if os.clock() - start_time >= request_timeout then
|
|
tcp:close()
|
|
|
|
show_error("Connection timeout")
|
|
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
------------------------------------------------------------
|
|
-- Construct HTTP request
|
|
------------------------------------------------------------
|
|
|
|
local request =
|
|
method .. " " .. path .. " HTTP/1.1\r\n" ..
|
|
"Host: " .. host .. ":" .. tostring(port) .. "\r\n" ..
|
|
"Connection: close\r\n"
|
|
|
|
if body then
|
|
request =
|
|
request ..
|
|
"Content-Type: application/json\r\n" ..
|
|
"Content-Length: " .. tostring(#body) .. "\r\n"
|
|
end
|
|
|
|
request = request .. "\r\n"
|
|
|
|
if body then
|
|
request = request .. body
|
|
end
|
|
|
|
|
|
------------------------------------------------------------
|
|
-- Send
|
|
------------------------------------------------------------
|
|
|
|
local send_pos = 1
|
|
|
|
while send_pos <= #request do
|
|
|
|
local sent, send_err, last_pos =
|
|
tcp:send(request, send_pos)
|
|
|
|
if sent then
|
|
send_pos = sent + 1
|
|
|
|
elseif send_err == "timeout" then
|
|
|
|
if last_pos then
|
|
send_pos = last_pos + 1
|
|
end
|
|
|
|
yield()
|
|
|
|
else
|
|
tcp:close()
|
|
|
|
show_error(
|
|
"Send failed: " ..
|
|
tostring(send_err)
|
|
)
|
|
|
|
return nil
|
|
end
|
|
|
|
if os.clock() - start_time >= request_timeout then
|
|
tcp:close()
|
|
|
|
show_error("Send timeout")
|
|
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
------------------------------------------------------------
|
|
-- Receive
|
|
------------------------------------------------------------
|
|
|
|
local response = {}
|
|
|
|
while true do
|
|
|
|
local data, recv_err, partial =
|
|
tcp:receive(4096)
|
|
|
|
if data then
|
|
response[#response + 1] = data
|
|
end
|
|
|
|
if partial then
|
|
response[#response + 1] = partial
|
|
end
|
|
|
|
if recv_err == "closed" then
|
|
break
|
|
|
|
elseif recv_err == "timeout" then
|
|
-- Nothing available yet.
|
|
yield()
|
|
|
|
elseif recv_err then
|
|
tcp:close()
|
|
|
|
show_error(
|
|
"Receive failed: " ..
|
|
tostring(recv_err)
|
|
)
|
|
|
|
return nil
|
|
end
|
|
|
|
if os.clock() - start_time >= request_timeout then
|
|
tcp:close()
|
|
|
|
show_error("HTTP request timeout")
|
|
|
|
return nil
|
|
end
|
|
end
|
|
|
|
|
|
tcp:close()
|
|
|
|
|
|
------------------------------------------------------------
|
|
-- Parse HTTP response
|
|
------------------------------------------------------------
|
|
|
|
local raw_response = table.concat(response)
|
|
|
|
local header_end =
|
|
raw_response:find("\r\n\r\n", 1, true)
|
|
|
|
if not header_end then
|
|
show_error("Invalid HTTP response")
|
|
return nil
|
|
end
|
|
|
|
local headers =
|
|
raw_response:sub(1, header_end - 1)
|
|
|
|
local response_body =
|
|
raw_response:sub(header_end + 4)
|
|
|
|
local status_code =
|
|
tonumber(
|
|
headers:match(
|
|
"^HTTP/%d%.%d%s+(%d+)"
|
|
)
|
|
)
|
|
|
|
if not status_code then
|
|
show_error("Could not determine HTTP status")
|
|
return nil
|
|
end
|
|
|
|
if status_code < 200 or status_code >= 300 then
|
|
show_error(
|
|
"HTTP error " ..
|
|
tostring(status_code)
|
|
)
|
|
|
|
return nil
|
|
end
|
|
|
|
return response_body
|
|
end
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- JSON HTTP helpers
|
|
----------------------------------------------------------------
|
|
|
|
local function http_request_json(url, method, body)
|
|
|
|
local response_body =
|
|
http_request(url, method, body)
|
|
|
|
if not response_body then
|
|
return nil
|
|
end
|
|
|
|
local ok, response =
|
|
pcall(
|
|
json.decode,
|
|
response_body
|
|
)
|
|
|
|
if not ok or type(response) ~= "table" then
|
|
show_error(
|
|
"JSON error decoding: " ..
|
|
tostring(response_body)
|
|
)
|
|
|
|
return nil
|
|
end
|
|
|
|
return response
|
|
end
|
|
|
|
|
|
local function http_request_void(url, method)
|
|
return http_request(url, method)
|
|
end
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- Resolume
|
|
----------------------------------------------------------------
|
|
|
|
local function update_columns()
|
|
|
|
-- Echo("Resolume: fetching composition...")
|
|
|
|
local comp =
|
|
http_request_json(
|
|
base_url .. "/api/v1/composition",
|
|
"GET"
|
|
)
|
|
|
|
if not comp then
|
|
return
|
|
end
|
|
|
|
if not comp.columns then
|
|
show_error(
|
|
"Resolume response contains no columns"
|
|
)
|
|
|
|
return
|
|
end
|
|
|
|
columns = {}
|
|
|
|
for _, column in ipairs(comp.columns) do
|
|
|
|
if column.name and column.name.value then
|
|
columns[column.name.value] = column.id
|
|
end
|
|
end
|
|
|
|
-- Echo(
|
|
-- "Resolume columns: " ..
|
|
-- json.encode(columns)
|
|
-- )
|
|
end
|
|
|
|
|
|
local function connect_column(name)
|
|
|
|
local column_id = columns[name]
|
|
|
|
if not column_id then
|
|
show_error(
|
|
"Unknown column: " ..
|
|
tostring(name)
|
|
)
|
|
|
|
return
|
|
end
|
|
|
|
http_request_void(
|
|
base_url ..
|
|
"/api/v1/composition/columns/by-id/" ..
|
|
tostring(column_id) ..
|
|
"/connect",
|
|
|
|
"POST"
|
|
)
|
|
end
|
|
|
|
|
|
----------------------------------------------------------------
|
|
-- Plugin entry point
|
|
----------------------------------------------------------------
|
|
|
|
local function main(_display, rawArgs)
|
|
|
|
|
|
|
|
if rawArgs == nil or rawArgs == "" then
|
|
|
|
while true do
|
|
update_columns()
|
|
|
|
coroutine.yield(5)
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
local columnName = rawArgs
|
|
|
|
if not columnName then
|
|
show_error("No column specified")
|
|
return
|
|
end
|
|
|
|
|
|
connect_column(columnName)
|
|
end
|
|
|
|
update_columns()
|
|
|
|
return main |