initialize
This commit is contained in:
commit
5b23ea99fd
43 changed files with 2336 additions and 0 deletions
8
Dash/dash.lua
Normal file
8
Dash/dash.lua
Normal file
|
@ -0,0 +1,8 @@
|
|||
local m = peripheral.find("monitor")
|
||||
m.setTextScale(0.5)
|
||||
local pixelbox = require("./util/pixelbox_lite")
|
||||
local screen = pixelbox.new(m)
|
||||
screen:set_pixel(3, 2, 1)
|
||||
screen:render()
|
||||
|
||||
print(screen:get_size())
|
184
Dash/util/pixelbox_lite.lua
Normal file
184
Dash/util/pixelbox_lite.lua
Normal file
|
@ -0,0 +1,184 @@
|
|||
--[[
|
||||
Made by O. Caha (9551-Dev), licensed under MIT
|
||||
|
||||
Modified by Andrew_7_1
|
||||
]]
|
||||
|
||||
local PIXELBOX = {}
|
||||
local OBJECT = {}
|
||||
|
||||
local t_sort,t_cat,s_char = table.sort,table.concat,string.char
|
||||
local function sort(a,b) return a[2] > b[2] end
|
||||
|
||||
local distances = {
|
||||
{5,256,16,8,64,32},
|
||||
{4,16,16384,256,128},
|
||||
[4] ={4,64,1024,256,128},
|
||||
[8] ={4,512,2048,256,1},
|
||||
[16] ={4,2,16384,256,1},
|
||||
[32] ={4,8192,4096,256,1},
|
||||
[64] ={4,4,1024,256,1},
|
||||
[128] ={6,32768,256,1024,2048,4096,16384},
|
||||
[256] ={6,1,128,2,512,4,8192},
|
||||
[512] ={4,8,2048,256,128},
|
||||
[1024] ={4,4,64,128,32768},
|
||||
[2048] ={4,512,8,128,32768},
|
||||
[4096] ={4,8192,32,128,32768},
|
||||
[8192] ={3,32,4096,256128},
|
||||
[16384]={4,2,16,128,32768},
|
||||
[32768]={5,128,1024,2048,4096,16384}
|
||||
}
|
||||
|
||||
local to_blit = {}
|
||||
for i = 0, 15 do
|
||||
to_blit[2^i] = ("%x"):format(i)
|
||||
end
|
||||
|
||||
|
||||
function PIXELBOX.RESTORE(BOX,color)
|
||||
local bc = {}
|
||||
|
||||
for y=1,BOX.height*3 do
|
||||
for x=1,BOX.width*2 do
|
||||
if not bc[y] then bc[y] = {} end
|
||||
bc[y][x] = color
|
||||
end
|
||||
end
|
||||
|
||||
BOX.CANVAS = bc
|
||||
end
|
||||
|
||||
local function build_drawing_char(a,b,c,d,e,f)
|
||||
local arr = {a,b,c,d,e,f}
|
||||
local c_types = {}
|
||||
local sortable = {}
|
||||
local ind = 0
|
||||
for i=1,6 do
|
||||
local c = arr[i]
|
||||
if not c_types[c] then
|
||||
ind = ind + 1
|
||||
c_types[c] = {0,ind}
|
||||
end
|
||||
|
||||
local t = c_types[c]
|
||||
local t1 = t[1] + 1
|
||||
|
||||
t[1] = t1
|
||||
sortable[t[2]] = {c,t1}
|
||||
end
|
||||
local n = #sortable
|
||||
while n > 2 do
|
||||
t_sort(sortable,sort)
|
||||
local bit6 = distances[sortable[n][1]]
|
||||
local index,run = 1,false
|
||||
local nm1 = n - 1
|
||||
for i=2,bit6[1] do
|
||||
if run then break end
|
||||
local tab = bit6[i]
|
||||
for j=1,nm1 do
|
||||
if sortable[j][1] == tab then
|
||||
index = j
|
||||
run = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local from,to = sortable[n][1],sortable[index][1]
|
||||
for i=1,6 do
|
||||
if arr[i] == from then
|
||||
arr[i] = to
|
||||
local sindex = sortable[index]
|
||||
sindex[2] = sindex[2] + 1
|
||||
end
|
||||
end
|
||||
|
||||
sortable[n] = nil
|
||||
n = n - 1
|
||||
end
|
||||
|
||||
local n = 128
|
||||
local a6 = arr[6]
|
||||
|
||||
if arr[1] ~= a6 then n = n + 1 end
|
||||
if arr[2] ~= a6 then n = n + 2 end
|
||||
if arr[3] ~= a6 then n = n + 4 end
|
||||
if arr[4] ~= a6 then n = n + 8 end
|
||||
if arr[5] ~= a6 then n = n + 16 end
|
||||
|
||||
if sortable[1][1] == arr[6] then
|
||||
return s_char(n),sortable[2][1],arr[6]
|
||||
else
|
||||
return s_char(n),sortable[1][1],arr[6]
|
||||
end
|
||||
end
|
||||
|
||||
function OBJECT:render()
|
||||
local t = self.term
|
||||
local blit_line,set_cursor = t.blit,t.setCursorPos
|
||||
|
||||
local w_double = self.width*2
|
||||
local canv = self.CANVAS
|
||||
|
||||
local sy = 0
|
||||
for y=1,self.height*3,3 do
|
||||
sy = sy + 1
|
||||
local layer_1 = canv[y]
|
||||
local layer_2 = canv[y+1]
|
||||
local layer_3 = canv[y+2]
|
||||
local char_line,fg_line,bg_line = {},{},{}
|
||||
local n = 0
|
||||
for x=1,w_double,2 do
|
||||
local xp1 = x+1
|
||||
local b11,b21,b12,b22,b13,b23 =
|
||||
layer_1[x],layer_1[xp1],
|
||||
layer_2[x],layer_2[xp1],
|
||||
layer_3[x],layer_3[xp1]
|
||||
|
||||
local char,fg,bg = " ",1,b11
|
||||
if not (b21 == b11
|
||||
and b12 == b11
|
||||
and b22 == b11
|
||||
and b13 == b11
|
||||
and b23 == b11) then
|
||||
char,fg,bg = build_drawing_char(b11,b21,b12,b22,b13,b23)
|
||||
end
|
||||
n = n + 1
|
||||
char_line[n] = char
|
||||
fg_line [n] = to_blit[fg]
|
||||
bg_line [n] = to_blit[bg]
|
||||
end
|
||||
|
||||
set_cursor(1,sy)
|
||||
blit_line(
|
||||
t_cat(char_line,""),
|
||||
t_cat(fg_line,""),
|
||||
t_cat(bg_line,"")
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
function OBJECT:clear(color)
|
||||
PIXELBOX.RESTORE(self,color)
|
||||
end
|
||||
|
||||
function OBJECT:set_pixel(x,y,color)
|
||||
self.CANVAS[y][x] = color
|
||||
end
|
||||
|
||||
function OBJECT:get_size()
|
||||
return #(self.CANVAS), #(self.CANVAS[1])
|
||||
end
|
||||
|
||||
function PIXELBOX.new(terminal,bg)
|
||||
local bg = bg or terminal.getBackgroundColor() or colors.black
|
||||
local BOX = {}
|
||||
local w,h = terminal.getSize()
|
||||
BOX.term = terminal
|
||||
setmetatable(BOX,{__index = OBJECT})
|
||||
BOX.width = w
|
||||
BOX.height = h
|
||||
PIXELBOX.RESTORE(BOX,bg)
|
||||
return BOX
|
||||
end
|
||||
|
||||
return PIXELBOX
|
19
Dash/widgets/ART.md
Normal file
19
Dash/widgets/ART.md
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
|
||||
Changes through time:
|
||||
* Morning
|
||||
* Day
|
||||
* Evening
|
||||
* Night
|
||||
|
||||
Changes with weather:
|
||||
* Sunny
|
||||
* Cloudy
|
||||
* Rainy
|
||||
* Stormy (diff from rain?)
|
||||
* Snowy - alternative to rainy
|
||||
|
||||
Changes with dates:
|
||||
<this assumes some things present on the scene>
|
||||
Fireworks
|
||||
Lowered flag
|
10
Dash/widgets/README.md
Normal file
10
Dash/widgets/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Widgets I'll need
|
||||
target: 5x3 monitors big (200x114 pixels allegedly)
|
||||
|
||||
### Essential
|
||||
* Time (with zones?)
|
||||
* Weather
|
||||
* Some kind of feed?
|
||||
|
||||
### Cool
|
||||
* Dynamic cityscape that changes with time and weather
|
1
GLONASS/README.md
Normal file
1
GLONASS/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
I originally made this project in 2022 as a proof of concept.
|
63
GLONASS/UNS.lua
Normal file
63
GLONASS/UNS.lua
Normal file
|
@ -0,0 +1,63 @@
|
|||
--[[
|
||||
Unitary Navigational System v0.0
|
||||
|
||||
This basic script attempts to consolidate a GPS constellation into a single computer. Because using 4 is plain wasteful!
|
||||
It was made out of project "GLONASS Tower" I made in 2022 after discovering CC
|
||||
|
||||
TODO: Bring back cool logging!
|
||||
]]
|
||||
|
||||
-- Modems declaration. Edit this
|
||||
local modems = {
|
||||
{
|
||||
id = "modem_id_here",
|
||||
pos = {
|
||||
x = 1,
|
||||
y = 1,
|
||||
z = 1
|
||||
},
|
||||
modem = nil,
|
||||
primary = true -- Highest modem, to increase coverage ever so slightly
|
||||
}
|
||||
}
|
||||
|
||||
-- Prepare all modems
|
||||
local primary_modem = nil
|
||||
for i = 1 .. #modems do
|
||||
modems[i].modem = peripheral.wrap(modems[i].id)
|
||||
if modems[i].primary then
|
||||
primary_modem = modems[i].modem
|
||||
end
|
||||
end
|
||||
if primary_modem == nil then
|
||||
primary_modem = modems[0].modem
|
||||
end
|
||||
primary_modem.open(gps.CHANNEL_GPS) -- Only *listen* on 1 modem to avoid repeats
|
||||
|
||||
-- Simple logging. It's not ideal but will do as a "temporary" solution
|
||||
local function log(message)
|
||||
term.write(message)
|
||||
term.setCursorPos(1, select(2, term.getCursorPos()) + 1)
|
||||
if select(2, term.getCursorPos()) >= select(2, term.getSize()) then
|
||||
term.scroll(1)
|
||||
term.setCursorPos(1, select(2, term.getCursorPos()) - 1)
|
||||
end
|
||||
end
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
log("UNS v0.0 starting...")
|
||||
|
||||
while true do
|
||||
-- Wait for a GPS query
|
||||
local event, side, channel, replyChannel, message, distance
|
||||
repeat
|
||||
event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
|
||||
until channel == gps.CHANNEL_GPS and message == "PING"
|
||||
log("Recieved request, distance: " .. distance)
|
||||
|
||||
-- Send data from all modems
|
||||
for _, modem in ipairs(modems) do
|
||||
modem.transmit(channel, replyChannel, modem.pos)
|
||||
log("Modem ID <" .. modem.id .. "> transmitted location")
|
||||
end
|
||||
end
|
95
MusicPlayer/Client/client.lua
Normal file
95
MusicPlayer/Client/client.lua
Normal file
|
@ -0,0 +1,95 @@
|
|||
local graphics = require("graphics")
|
||||
|
||||
local modem = peripheral.find('modem')
|
||||
if not modem then error('No modem found') end
|
||||
modem.open(7101)
|
||||
|
||||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local speaker = peripheral.find("speaker")
|
||||
if not speaker then error('No speaker found') end
|
||||
local decoder = dfpwm.make_decoder()
|
||||
|
||||
local status_vars = {
|
||||
is_playing = true,
|
||||
is_paused = false,
|
||||
current_second = 0,
|
||||
track_index = 1,
|
||||
start_index = 1,
|
||||
}
|
||||
|
||||
local cfg_vars = {
|
||||
forward_skip = 5,
|
||||
backward_skip = 5,
|
||||
port = 7101,
|
||||
chunk_size = 0.5
|
||||
}
|
||||
|
||||
local function get_catalogue()
|
||||
modem.transmit(7101, 7101, {type = 'query'})
|
||||
local event, sides, channel, replyChannel, message, distance = os.pullEvent('modem_message')
|
||||
return message.audio_catalogue
|
||||
end
|
||||
local audio_catalogue = get_catalogue()
|
||||
|
||||
local function music_loop()
|
||||
local play_music = require("playback")
|
||||
while true do
|
||||
status_vars.current_second = play_music(status_vars, cfg_vars, audio_catalogue, modem)
|
||||
sleep()
|
||||
end
|
||||
end
|
||||
|
||||
local function draw_ui_loop()
|
||||
while true do
|
||||
if not status_vars.is_playing then
|
||||
graphics.draw_track_list(1, 1, audio_catalogue, status_vars.start_index, select(2, term.getSize()) - 1, term)
|
||||
else
|
||||
graphics.draw_track_list(1, 1, audio_catalogue, status_vars.start_index, select(2, term.getSize()) - 4, term)
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function control_loop()
|
||||
while true do
|
||||
local event, button, x, y = os.pullEvent("mouse_click")
|
||||
|
||||
local btn_actions = {
|
||||
pause = {
|
||||
x = x + math.floor((select(1, term.getSize()) - 11) / 2) + 5,
|
||||
y = select(2, term.getSize()) - 1,
|
||||
func = function ()
|
||||
status_vars.is_paused = not status_vars.is_paused
|
||||
end
|
||||
},
|
||||
forward = {
|
||||
x = math.floor((select(1, term.getSize()) - 11) / 2) + 9,
|
||||
y = select(2, term.getSize()) - 1,
|
||||
func = function ()
|
||||
status_vars.current_second = status_vars.current_second + cfg_vars.forward_skip
|
||||
end
|
||||
},
|
||||
backward = {
|
||||
x = math.floor((select(1, term.getSize()) - 11) / 2) + 1,
|
||||
y = select(2, term.getSize()) - 1,
|
||||
func = function ()
|
||||
status_vars.current_second = status_vars.current_second - cfg_vars.backward_skip
|
||||
end
|
||||
}
|
||||
}
|
||||
if x == 3 and y == 1 then return end
|
||||
|
||||
term.setCursorPos(btn_actions.pause.x, btn_actions.pause.y)
|
||||
term.write('%')
|
||||
|
||||
for i, v in pairs(btn_actions) do
|
||||
if x == v.x and y == v.y then
|
||||
v.func()
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
term.clear() -- Clear terminal
|
||||
parallel.waitForAny(music_loop, draw_ui_loop, control_loop) -- Start loops
|
167
MusicPlayer/Client/graphics.lua
Normal file
167
MusicPlayer/Client/graphics.lua
Normal file
|
@ -0,0 +1,167 @@
|
|||
local api = {}
|
||||
|
||||
api.symbols = {
|
||||
audio_controls = {
|
||||
play = "\16",
|
||||
pause = "\19",
|
||||
forward = "\16",
|
||||
backward = "\17",
|
||||
full_back = "\171"
|
||||
},
|
||||
app_controls = {
|
||||
up = "\30",
|
||||
down = "\31",
|
||||
download = "\25",
|
||||
play = "\16"
|
||||
},
|
||||
misc = {
|
||||
music = "\15",
|
||||
}
|
||||
}
|
||||
|
||||
-- Draw a spinner loop at a specified position. Useful for waitForAny.
|
||||
function api.spinner_loop(x, y, spin_monitor)
|
||||
local spin_chars = {
|
||||
"\139",
|
||||
"\138",
|
||||
"\142",
|
||||
"\140",
|
||||
"\141",
|
||||
"\133",
|
||||
"\135",
|
||||
"\131"
|
||||
}
|
||||
local index = 1
|
||||
while true do
|
||||
spin_monitor.setCursorPos(x, y)
|
||||
spin_monitor.write(spin_chars[index])
|
||||
index = index + 1
|
||||
if index > #spin_chars then index = index - #spin_chars end
|
||||
sleep(0.1)
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
Function to create simple progress bars of any length.
|
||||
size - length of the bar in pixels, taken, max - ratio of the bar that should be full.
|
||||
|
||||
There is an ability to set custom symbols for full and empty pixels,
|
||||
however it is not necessary and by default the program uses '#' and '.' for these
|
||||
]]
|
||||
function api.make_progressbar(size, taken, max, char_full, char_empty)
|
||||
--[[
|
||||
Maths behind the function are quite simple:
|
||||
taken/max = x/size
|
||||
taken * size = x * max
|
||||
x = taken * size / max
|
||||
]]
|
||||
local full_symbol, empty_symbol = char_full or '#', char_empty or '.'
|
||||
size = size - 2 -- make space for "[" and "]"
|
||||
local progress = math.floor(size * taken / max)
|
||||
local hash = (full_symbol):rep(progress)
|
||||
local dots = (empty_symbol):rep(size - progress)
|
||||
return ("[" .. hash .. dots .. "]")
|
||||
end
|
||||
|
||||
local function seconds_to_string(seconds)
|
||||
-- int to mm:ss
|
||||
local minutes = math.floor(seconds / 60)
|
||||
local seconds = seconds - minutes * 60
|
||||
return string.format("%02d:%02d", minutes, seconds)
|
||||
end
|
||||
|
||||
function api.draw_audio_bar(x, y, progress_seconds, track_info, is_paused, monitor)
|
||||
--[[
|
||||
Example:
|
||||
| Among us soundtrack lol |
|
||||
| [<] [####........[<]............] [>] |
|
||||
| 12:43/14:31 |
|
||||
]]
|
||||
local x_max, y_max = monitor.getSize()
|
||||
|
||||
-- Title
|
||||
monitor.setCursorPos(x, y)
|
||||
local title = track_info.title
|
||||
if #title > x_max - 2 then
|
||||
title = title:sub(1, x_max - 4) .. ".."
|
||||
end
|
||||
monitor.write((' '):rep(math.floor((x_max - #title) / 2)) .. title .. (' '):rep(math.ceil((x_max - #title) / 2)))
|
||||
|
||||
-- Progress bar
|
||||
monitor.setCursorPos(x, y + 1)
|
||||
local progress_bar = api.make_progressbar(x_max - 16, progress_seconds, track_info.length)
|
||||
--monitor.blit(' ' .. seconds_to_string(progress_seconds) .. ' ' .. progress_bar .. ' ' .. seconds_to_string(track_info.length), 'f00000f0' .. ('3'):rep(x_max - 16) .. '0f00000f', ('f'):rep(x_max))
|
||||
monitor.write(' ' .. seconds_to_string(progress_seconds) .. ' ' .. progress_bar .. ' ' .. seconds_to_string(track_info.length))
|
||||
|
||||
-- Controls
|
||||
local controls = {
|
||||
api.symbols.audio_controls.backward,
|
||||
is_paused and api.symbols.audio_controls.play or api.symbols.audio_controls.pause,
|
||||
api.symbols.audio_controls.forward
|
||||
}
|
||||
for i, control in ipairs(controls) do
|
||||
monitor.setCursorPos(x + math.floor((x_max - 11) / 2) + (i - 1) * 4, y + 1)
|
||||
monitor.write('[' .. control .. ']')
|
||||
end
|
||||
end
|
||||
|
||||
-- Copy of draw_audio_bar, but for when no track is played - just a blank bar.
|
||||
function api.draw_track_list(x, y, audio_catalogue, start_index, height, monitor)
|
||||
height = height or select(2, monitor.getSize())
|
||||
local x_max, y_max = monitor.getSize()
|
||||
|
||||
--[[
|
||||
top bar:
|
||||
"[X] 21:00:01 Tue 12"
|
||||
|
||||
-button to close the app
|
||||
-time and date
|
||||
|
||||
bottom bar:
|
||||
"[up_btn] [down_btn] Showing [current_max_index - list_size]-[current_max_index] of [#audio_catalogue] | [REFRESH]"
|
||||
-buttons to go up and down the list
|
||||
-current index and total number of tracks
|
||||
-button to refresh the list
|
||||
|
||||
list items:
|
||||
"[index] [title] [length] [play_btn] [download_btn]"
|
||||
-index - number of the track in the list
|
||||
-title - title of the track
|
||||
-length - length of the track
|
||||
-play_btn - button to play the track
|
||||
-download_btn - button to download the track (not implemented yet)
|
||||
]]
|
||||
|
||||
-- top bar
|
||||
monitor.setCursorPos(x, y)
|
||||
local current_time = os.date("%H:%M:%S %b %d")
|
||||
local exit_btn = "[X]"
|
||||
|
||||
monitor.blit(' ' .. exit_btn .. ' ' ..current_time, 'f0e0f00000000f000f00', 'ffffffffffffffffffff')
|
||||
|
||||
-- bottom bar
|
||||
monitor.setCursorPos(x, y + height - 1)
|
||||
local bottom_btns = '[' .. api.symbols.app_controls.up .. '] [' .. api.symbols.app_controls.down .. ']'
|
||||
local showing = 'Showing ' .. tostring(start_index) .. '-' .. tostring(start_index + height - 2) .. ' of ' .. tostring(#audio_catalogue)
|
||||
local refresh_btn = '[REFRESH]'
|
||||
monitor.blit(' ' .. bottom_btns .. ' ' .. showing .. ' | ' .. refresh_btn, 'f0e0f0b0f' .. ('0'):rep(#showing) .. "f0f033333330", ('f'):rep(#bottom_btns + #showing + #refresh_btn + 5))
|
||||
-- monitor.write(' ' .. bottom_btns .. ' ' .. showing .. ' | ' .. refresh_btn)
|
||||
|
||||
-- list items
|
||||
-- i should be current_max_index - height - 2 or 1 if current_max_index - height - 2 < 1
|
||||
for i = start_index, math.min(start_index + height - 2, #audio_catalogue) do
|
||||
if (y + 1 + i >= y + height - 1) or i > #audio_catalogue then break end
|
||||
|
||||
local track = audio_catalogue[i]
|
||||
local index = tostring(i)
|
||||
local title = track.title
|
||||
local length = seconds_to_string(track.length)
|
||||
local play_btn = api.symbols.audio_controls.play
|
||||
local download_btn = api.symbols.app_controls.download
|
||||
local list_item = ' ' .. index .. ' ' .. title .. ' ' .. length .. ' ' .. play_btn .. ' ' .. download_btn
|
||||
monitor.setCursorPos(x, y + 1 + i)
|
||||
monitor.write(list_item)
|
||||
end
|
||||
end
|
||||
|
||||
return api
|
27
MusicPlayer/Client/playback.lua
Normal file
27
MusicPlayer/Client/playback.lua
Normal file
|
@ -0,0 +1,27 @@
|
|||
local dfpwm = require("cc.audio.dfpwm")
|
||||
local speaker = peripheral.find("speaker")
|
||||
local decoder = dfpwm.make_decoder()
|
||||
|
||||
local graphics = require("graphics")
|
||||
|
||||
|
||||
local function play_music(status_vars, cfg_vars, audio_catalogue, modem)
|
||||
if (not status_vars.is_playing) or status_vars.is_paused then return 0 end
|
||||
|
||||
modem.transmit(7101, 7101, {type = 'track_chunk',
|
||||
index = status_vars.track_index,
|
||||
starting_second = status_vars.current_second,
|
||||
length = cfg_vars.chunk_size})
|
||||
local event, sides, channel, replyChannel, message, distance = os.pullEvent('modem_message')
|
||||
|
||||
local buffer = decoder(message.track)
|
||||
|
||||
while not speaker.playAudio(buffer) do
|
||||
os.pullEvent("speaker_audio_empty")
|
||||
graphics.draw_audio_bar(1, select(2, term.getSize()) - 2, status_vars.current_second + cfg_vars.chunk_size, audio_catalogue[status_vars.track_index], status_vars.is_paused, term)
|
||||
end
|
||||
|
||||
return status_vars.current_second + cfg_vars.chunk_size
|
||||
end
|
||||
|
||||
return play_music
|
59
MusicPlayer/Client/touch_processing.lua
Normal file
59
MusicPlayer/Client/touch_processing.lua
Normal file
|
@ -0,0 +1,59 @@
|
|||
local touch_processer = {}
|
||||
|
||||
touch_processer.init = function ()
|
||||
local self = {}
|
||||
|
||||
|
||||
|
||||
function self.process_minimised_controls()
|
||||
local x, y = e[3], e[4]
|
||||
local btn_actions = {
|
||||
pause = {
|
||||
x = pause_btn_coords.x,
|
||||
y = pause_btn_coords.y,
|
||||
func = function()
|
||||
status_vars.is_paused = not status_vars.is_paused
|
||||
end
|
||||
},
|
||||
forward = {
|
||||
x = forward_btn_coords.x,
|
||||
y = forward_btn_coords.y,
|
||||
func = function()
|
||||
status_vars.current_second = math.min(status_vars.current_second + cfg_vars.forward_skip, status_vars.max_seconds)
|
||||
end
|
||||
},
|
||||
backward = {
|
||||
x = backward_btn_coords.x,
|
||||
y = backward_btn_coords.y,
|
||||
func = function()
|
||||
status_vars.current_second = math.max(status_vars.current_second - cfg_vars.backward_skip, 0)
|
||||
end
|
||||
},
|
||||
exit = {
|
||||
x = exit_btn_coords.x,
|
||||
y = exit_btn_coords.y,
|
||||
func = function()
|
||||
status_vars.is_playing = false
|
||||
return
|
||||
end
|
||||
},
|
||||
minimise = {
|
||||
x = minimise_btn_coords.x,
|
||||
y = minimise_btn_coords.y,
|
||||
func = function()
|
||||
status_vars.is_minimised = not status_vars.is_minimised
|
||||
end
|
||||
},
|
||||
}
|
||||
for k, v in pairs(btn_actions) do
|
||||
if x == v.x and y == v.y then
|
||||
v.func()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local function key_to_func()
|
||||
end
|
98
MusicPlayer/Server/audio_manager.lua
Normal file
98
MusicPlayer/Server/audio_manager.lua
Normal file
|
@ -0,0 +1,98 @@
|
|||
--[[
|
||||
Class to manage ComputerCraft (CC) audio tracks on different disks
|
||||
|
||||
Written by Andrew_7_1
|
||||
]]
|
||||
|
||||
local audio_manager = {}
|
||||
audio_manager.init = function (dirs_file)
|
||||
local self = {}
|
||||
|
||||
-- Load audio directories from file where directories are seperated by newlines"
|
||||
local function load_audio_dirs(file_location)
|
||||
local audio_dirs = {}
|
||||
local file = io.open(file_location, 'r')
|
||||
if file then
|
||||
for line in file:lines() do
|
||||
table.insert(audio_dirs, line)
|
||||
end
|
||||
file:close()
|
||||
end
|
||||
return audio_dirs
|
||||
end
|
||||
local audio_dirs = load_audio_dirs(dirs_file or 'audio_dirs.txt')
|
||||
|
||||
--[[
|
||||
Iterate over all audio directories and load all audio files into a catalogue.
|
||||
Audio files have extensions of .dfpwm and rarely .wav
|
||||
Save them to a table of tables like this:
|
||||
{
|
||||
["filepath"] = absolute file path
|
||||
["length"] = file size / 6000
|
||||
["title"] = file name without extension and path, e.g. "title.dfpwm" -> "title"
|
||||
["extension"] = file extension, e.g. "dfpwm"
|
||||
}
|
||||
]]
|
||||
local function load_audio_catalogue()
|
||||
local audio_catalogue = {}
|
||||
for _, dir in pairs(audio_dirs) do
|
||||
for _, file in pairs(fs.list(dir)) do
|
||||
if file:find('.dfpwm') or file:find('.wav') then
|
||||
local file_path = dir .. '/' .. file
|
||||
local file_size = fs.getSize(file_path)
|
||||
local file_title = file:sub(1, #file - #file:match('.*%.(.*)$'))
|
||||
local file_extension = file:match('.*%.(.*)$')
|
||||
table.insert(audio_catalogue, {
|
||||
["filepath"] = file_path,
|
||||
["length"] = file_size / 6000,
|
||||
["title"] = file_title,
|
||||
["extension"] = file_extension
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
return audio_catalogue
|
||||
end
|
||||
self.audio_catalogue = load_audio_catalogue()
|
||||
|
||||
-- Update audio catalogue
|
||||
function self.update_catalogue()
|
||||
self.audio_catalogue = load_audio_catalogue()
|
||||
end
|
||||
|
||||
function self.get_catalogue()
|
||||
return self.audio_catalogue
|
||||
end
|
||||
|
||||
|
||||
-- Get part of an audio file in the catalogue
|
||||
function self.get_track_chunk(index, starting_second, length)
|
||||
if index > #self.audio_catalogue then
|
||||
return false
|
||||
end
|
||||
local track = self.audio_catalogue[index]
|
||||
|
||||
local file = io.open(track.filepath, 'rb')
|
||||
if not file then
|
||||
return false
|
||||
end
|
||||
|
||||
file:seek('set', starting_second * 6000)
|
||||
local chunk = file:read(length * 6000)
|
||||
file:close()
|
||||
|
||||
return chunk
|
||||
end
|
||||
|
||||
-- More convenient way to get track info than using entire catalogue
|
||||
function self.get_track_details(index)
|
||||
if index > #self.audio_catalogue then
|
||||
return false
|
||||
end
|
||||
return self.audio_catalogue[index]
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return audio_manager
|
60
MusicPlayer/Server/logger.lua
Normal file
60
MusicPlayer/Server/logger.lua
Normal file
|
@ -0,0 +1,60 @@
|
|||
|
||||
|
||||
local logger = {}
|
||||
logger.init = function(monitor_wrap, scale)
|
||||
local self = {}
|
||||
|
||||
local monitor = monitor_wrap or peripheral.find("monitor")
|
||||
if not monitor then
|
||||
monitor = term
|
||||
else monitor.setTextScale(scale or 0.5)
|
||||
end
|
||||
local log_colours = {
|
||||
['log'] = colours.white,
|
||||
['info'] = colours.lightBlue,
|
||||
['warn'] = colours.yellow,
|
||||
['err'] = colours.red,
|
||||
['success'] = colours.lime
|
||||
}
|
||||
|
||||
local function move_line()
|
||||
monitor.setCursorPos(1, select(2, monitor.getCursorPos()) + 1)
|
||||
if select(2, monitor.getCursorPos()) >= select(2, monitor.getSize()) then
|
||||
monitor.scroll(1)
|
||||
monitor.setCursorPos(1, select(2, monitor.getCursorPos()) - 1)
|
||||
end
|
||||
end
|
||||
|
||||
local function write_msg(message, msg_colour)
|
||||
local time_str = os.date('%Y-%b-%d %H:%M:%S')
|
||||
local msg_formatted = "[" .. time_str .. "] " .. message
|
||||
|
||||
monitor.setTextColour(msg_colour)
|
||||
monitor.write(msg_formatted)
|
||||
move_line()
|
||||
end
|
||||
|
||||
function self.log(msg)
|
||||
write_msg(msg, log_colours['log'])
|
||||
end
|
||||
|
||||
function self.info(msg)
|
||||
write_msg('INFO: ' .. msg, log_colours['info'])
|
||||
end
|
||||
|
||||
function self.warning(msg)
|
||||
write_msg('WARNING: ' .. msg, log_colours['warn'])
|
||||
end
|
||||
|
||||
function self.error(msg)
|
||||
write_msg('ERROR: ' .. msg, log_colours['err'])
|
||||
end
|
||||
|
||||
function self.success(msg)
|
||||
write_msg('SUCCESS: ' .. msg, log_colours['success'])
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return logger
|
60
MusicPlayer/Server/server.lua
Normal file
60
MusicPlayer/Server/server.lua
Normal file
|
@ -0,0 +1,60 @@
|
|||
--[[
|
||||
Audio server for ComputerCraft (CC)
|
||||
|
||||
Written by Andrew_7_1
|
||||
]]
|
||||
|
||||
local server = {}
|
||||
server.init = function (port, audio_dir)
|
||||
local self = {}
|
||||
|
||||
local logger = require('logger').init()
|
||||
local audio_manager = require('audio_manager').init(audio_dir or 'audio_dirs.txt')
|
||||
|
||||
local port = port or 7101
|
||||
local modem = peripheral.find('modem')
|
||||
if not modem then error('No modem found') end
|
||||
modem.open(port)
|
||||
logger.info('Audio server started on port ' .. port .. ' with audio directories from ' .. audio_dir)
|
||||
|
||||
local message_types = {
|
||||
query = function (replyChannel, msg)
|
||||
modem.transmit(replyChannel, port, {code = 200, audio_catalogue = audio_manager.get_catalogue()})
|
||||
logger.success('Query sent')
|
||||
end,
|
||||
refresh = function (replyChannel, msg)
|
||||
audio_manager.update_catalogue()
|
||||
modem.transmit(replyChannel, port, {code = 200})
|
||||
logger.success('Catalogue updated')
|
||||
end,
|
||||
|
||||
track_info = function (replyChannel, msg)
|
||||
local track = audio_manager.get_track_details(msg.index)
|
||||
if not track then
|
||||
modem.transmit(replyChannel, port, {code = 404})
|
||||
logger.error('Track not found (id: ' .. msg.index .. ')')
|
||||
else
|
||||
modem.transmit(replyChannel, port, {code = 200, track = track})
|
||||
logger.success('Track details sent (id: ' .. msg.index .. ')')
|
||||
end
|
||||
end,
|
||||
track_chunk = function (replyChannel, msg)
|
||||
local track = audio_manager.get_track_chunk(msg.index, msg.starting_second, msg.length)
|
||||
if not track then
|
||||
modem.transmit(replyChannel, port, {code = 404})
|
||||
logger.error('Track not found (id: ' .. msg.index .. ')')
|
||||
else
|
||||
modem.transmit(replyChannel, port, {code = 200, track = track})
|
||||
logger.success('Part of a track sent (id: ' .. msg.index .. ', part:' .. msg.starting_second .. '-' .. msg.starting_second + msg.length .. ')')
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
function self.process_transmission(event, side, channel, replyChannel, message, distance)
|
||||
message_types[message.type](replyChannel, message)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return server
|
14
MusicPlayer/Server/startup.lua
Normal file
14
MusicPlayer/Server/startup.lua
Normal file
|
@ -0,0 +1,14 @@
|
|||
local config = {
|
||||
port = 7101,
|
||||
audio_dir = 'audio_dirs.txt'
|
||||
}
|
||||
|
||||
local server = require("server").init(config.port, config.audio_dir)
|
||||
local modem = peripheral.find("modem")
|
||||
if not modem then error("No modem found") end
|
||||
modem.open(config.port)
|
||||
|
||||
while true do
|
||||
local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message")
|
||||
server.process_transmission(event, side, channel, replyChannel, message, distance)
|
||||
end
|
4
README.md
Normal file
4
README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# CC Projects
|
||||
Collection of all my ComputerCraft projects, as none of them are big or finished enough to warrant separate repositories.
|
||||
|
||||
If you think that this is a disorganized mess then you are 100% correct.
|
128
RSS/feeds.lua
Normal file
128
RSS/feeds.lua
Normal file
|
@ -0,0 +1,128 @@
|
|||
local FEEDS = {
|
||||
feeds = {}
|
||||
}
|
||||
|
||||
local FEEDS_PATH = "./feeds"
|
||||
local SLAXML = require('slaxml')
|
||||
local LOG = require('log')
|
||||
|
||||
-- Retrieve feeds from config files and prepare them for use
|
||||
function FEEDS:Load()
|
||||
self.feeds = {}
|
||||
|
||||
local files = fs.find(FEEDS_PATH .. "/*.json")
|
||||
for _, v in ipairs(files) do
|
||||
local file = fs.open(v, "r")
|
||||
local contents = file.readAll()
|
||||
local feed = textutils.unserialiseJSON(contents)
|
||||
|
||||
local response, err, _ = http.get(feed.link)
|
||||
if response == nil then
|
||||
LOG:Error("HTTP Error loading feed (" .. v .. "): " .. err)
|
||||
else
|
||||
local xml = response.readAll()
|
||||
response.close()
|
||||
local element
|
||||
local in_item = false
|
||||
local function startElement(name, nsURI, nsPrefix)
|
||||
-- Track the current element name
|
||||
element = name
|
||||
if element == "item" then
|
||||
in_item = true
|
||||
end
|
||||
end
|
||||
local function text(text, cdata)
|
||||
-- Store the text content based on the current element name
|
||||
if element == 'title' and not feed.title then
|
||||
feed.title = text
|
||||
elseif in_item and not feed.last and element == 'guid' then
|
||||
feed.last = text
|
||||
end
|
||||
end
|
||||
local parser = SLAXML:parser{
|
||||
startElement = startElement,
|
||||
text = text
|
||||
}
|
||||
parser:parse(xml)
|
||||
|
||||
if feed.title_override then
|
||||
feed.title = feed.title_override
|
||||
end
|
||||
|
||||
table.insert(self.feeds, feed)
|
||||
LOG:Info("Loaded feed " .. feed.title)
|
||||
end
|
||||
end
|
||||
if #self.feeds == 0 then
|
||||
LOG:Info("No feeds loaded, nothing to do")
|
||||
return true
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function FEEDS:ParseLatest()
|
||||
local output = {}
|
||||
for _, feed in ipairs(self.feeds) do
|
||||
local response, err, _ = http.get(feed.link)
|
||||
if response == nil then
|
||||
LOG:Error("HTTP Error fetching feed: " .. err)
|
||||
else
|
||||
local xml = response.readAll()
|
||||
response.close()
|
||||
|
||||
local element
|
||||
local new_last
|
||||
local done = false
|
||||
local current_item = {
|
||||
colour = feed.colour,
|
||||
source = feed.title
|
||||
}
|
||||
local in_item = false
|
||||
local function startElement(name, nsURI, nsPrefix)
|
||||
if done then return end
|
||||
|
||||
-- Track the current element name
|
||||
element = name
|
||||
if element == "item" then
|
||||
in_item = true
|
||||
current_item = {
|
||||
colour = feed.colour,
|
||||
source = feed.title
|
||||
}
|
||||
end
|
||||
end
|
||||
local function closeElement(name, nsURI, nsPrefix)
|
||||
if done then return end
|
||||
|
||||
if name == 'item' and not done then
|
||||
table.insert(output, current_item)
|
||||
end
|
||||
end
|
||||
local function text(text, cdata)
|
||||
if done or not in_item then return end
|
||||
|
||||
-- Store the text content based on the current element name
|
||||
if element == 'title' then
|
||||
current_item.title = text
|
||||
elseif in_item and element == 'guid' then
|
||||
if not new_last then
|
||||
new_last = text
|
||||
end
|
||||
if feed.last == text then
|
||||
done = true
|
||||
end
|
||||
end
|
||||
end
|
||||
local parser = SLAXML:parser{
|
||||
startElement = startElement,
|
||||
closeElement = closeElement,
|
||||
text = text
|
||||
}
|
||||
parser:parse(xml)
|
||||
feed.last = new_last
|
||||
end
|
||||
end
|
||||
return output
|
||||
end
|
||||
|
||||
return FEEDS
|
5
RSS/feeds/nytimes.json
Normal file
5
RSS/feeds/nytimes.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://rss.nytimes.com/services/xml/rss/nyt/Technology.xml",
|
||||
"title_override": "NYT",
|
||||
"colour": "9"
|
||||
}
|
5
RSS/feeds/tass.json
Normal file
5
RSS/feeds/tass.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://tass.ru/rss/v2.xml",
|
||||
"title_override": "TACC",
|
||||
"colour": "b"
|
||||
}
|
5
RSS/feeds/youtube_generic.json
Normal file
5
RSS/feeds/youtube_generic.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"link": "https://www.youtube.com/@tag",
|
||||
"title_override": "Youtuber name",
|
||||
"colour": "e"
|
||||
}
|
71
RSS/graphics.lua
Normal file
71
RSS/graphics.lua
Normal file
|
@ -0,0 +1,71 @@
|
|||
local GRAPHICS = {
|
||||
target = nil,
|
||||
contents = {}
|
||||
}
|
||||
|
||||
local LOG = require('log')
|
||||
|
||||
-- Set target's colorscheme to gruvbox for better visuals
|
||||
function GRAPHICS:SetColours()
|
||||
if self.target == nil then
|
||||
return
|
||||
end
|
||||
self.target.setPaletteColour(colours.red, 0xfb4934)
|
||||
self.target.setPaletteColour(colours.blue, 0x83a598)
|
||||
self.target.setPaletteColour(colours.green, 0xb8bb26)
|
||||
self.target.setPaletteColour(colours.purple, 0xd3869b)
|
||||
self.target.setPaletteColour(colours.cyan, 0x8ec07c)
|
||||
self.target.setPaletteColour(colours.white, 0xf9f5d7)
|
||||
self.target.setPaletteColour(colours.black, 0x1d2021)
|
||||
end
|
||||
|
||||
-- Set target output for our graphics
|
||||
function GRAPHICS:SetTarget(target)
|
||||
self.target = target
|
||||
if self.target == nil then
|
||||
LOG:Error("Nil display target, items will not display")
|
||||
end
|
||||
target.clear()
|
||||
target.setTextScale(0.5)
|
||||
self:Refresh(true)
|
||||
end
|
||||
|
||||
function GRAPHICS:Refresh(full)
|
||||
if self.target == nil then
|
||||
return
|
||||
end
|
||||
local _, y = self.target.getSize()
|
||||
while #self.contents > y do
|
||||
table.remove(self.contents, 1)
|
||||
end
|
||||
|
||||
if full then
|
||||
self.target.scroll(#self.contents)
|
||||
for k, entry in ipairs(self.contents) do
|
||||
self.target.setCursorPos(1, y - k + 1)
|
||||
local output = "[" .. entry.source .. "] " .. entry.title
|
||||
local fg_blit = "0" .. (entry.colour):rep(#entry.source) .. ("0"):rep(#entry.title + 2)
|
||||
self.target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
end
|
||||
else
|
||||
self.target.scroll(1)
|
||||
self.target.setCursorPos(1, y)
|
||||
local entry = self.contents[#self.contents]
|
||||
local output = "[" .. entry.source .. "] " .. entry.title
|
||||
local fg_blit = "0" .. (entry.colour):rep(#entry.source) .. ("0"):rep(#entry.title + 2)
|
||||
self.target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
end
|
||||
end
|
||||
|
||||
function GRAPHICS:AddEntry(entry)
|
||||
table.insert(self.contents, entry)
|
||||
self:Refresh()
|
||||
end
|
||||
|
||||
function GRAPHICS:Add(entries)
|
||||
for _, entry in ipairs(entries) do
|
||||
self:AddEntry(entry)
|
||||
end
|
||||
end
|
||||
|
||||
return GRAPHICS
|
98
RSS/log.lua
Normal file
98
RSS/log.lua
Normal file
|
@ -0,0 +1,98 @@
|
|||
local LOG = {
|
||||
target = term,
|
||||
history = {},
|
||||
history_cutoff = 50,
|
||||
log_path = "./logs/",
|
||||
debug = false
|
||||
}
|
||||
|
||||
-- Set target's colorscheme to gruvbox for better visuals
|
||||
function LOG:SetColours()
|
||||
self.target.setPaletteColour(colours.red, 0xfb4934)
|
||||
self.target.setPaletteColour(colours.blue, 0x83a598)
|
||||
self.target.setPaletteColour(colours.green, 0xb8bb26)
|
||||
self.target.setPaletteColour(colours.purple, 0xd3869b)
|
||||
self.target.setPaletteColour(colours.cyan, 0x8ec07c)
|
||||
self.target.setPaletteColour(colours.white, 0xf9f5d7)
|
||||
self.target.setPaletteColour(colours.black, 0x1d2021)
|
||||
end
|
||||
|
||||
-- Set target output for our log
|
||||
function LOG:SetTarget(target)
|
||||
self.target = target
|
||||
target.clear()
|
||||
target.setTextScale(0.5)
|
||||
end
|
||||
|
||||
function LOG:SetDebug(value)
|
||||
self.debug = value
|
||||
end
|
||||
|
||||
-- Utility function to output some kind of log message
|
||||
local function genericLog(log, level, message, col)
|
||||
if #level ~= 4 or #col ~= 1 then
|
||||
LOG:Error("Incorrect generic log format")
|
||||
return
|
||||
end
|
||||
|
||||
local target = log.target
|
||||
target.scroll(1)
|
||||
local _, y = target.getSize()
|
||||
target.setCursorPos(1, y)
|
||||
|
||||
local time = os.date("%T", os.epoch("local") / 1000)
|
||||
local output = "[" .. time .. "|" .. level .. "] " .. message
|
||||
local fg_blit = ("0"):rep(#time + 2) .. (col):rep(4) .. ("0"):rep(#message + 2)
|
||||
target.blit(output, fg_blit, ("f"):rep(#output))
|
||||
|
||||
table.insert(log.history, output)
|
||||
if #log.history > log.history_cutoff then
|
||||
table.remove(log.history, 1)
|
||||
end
|
||||
end
|
||||
|
||||
function LOG:Info(message)
|
||||
genericLog(self, "INFO", message, "b")
|
||||
end
|
||||
|
||||
function LOG:Success(message)
|
||||
genericLog(self, "SCCS", message, "d")
|
||||
end
|
||||
|
||||
function LOG:Debug(message)
|
||||
if not self.debug then return end
|
||||
genericLog(self, "DBUG", message, "8")
|
||||
end
|
||||
|
||||
function LOG:Warning(message)
|
||||
genericLog(self, "WARN", message, "4")
|
||||
end
|
||||
|
||||
function LOG:Error(message)
|
||||
genericLog(self, "ERRO", message, "e")
|
||||
end
|
||||
|
||||
function LOG:Critical(message)
|
||||
genericLog(self, "CRIT", message, "e")
|
||||
self:Dump()
|
||||
error("critical exception occured, read above")
|
||||
end
|
||||
|
||||
function LOG:Clear()
|
||||
self.history = {}
|
||||
end
|
||||
|
||||
-- Put all recent log messages into a file for later study
|
||||
function LOG:Dump()
|
||||
local out = ""
|
||||
for _, v in ipairs(self.history) do
|
||||
out = out + v .. "\n"
|
||||
end
|
||||
local time = os.date("%F-%T", os.epoch("local") / 1000)
|
||||
local file = fs.open(string.format("%s/dump_%s.log", self.log_path, time), "w")
|
||||
file.write(out)
|
||||
file.close()
|
||||
return out
|
||||
end
|
||||
|
||||
return LOG
|
28
RSS/main.lua
Normal file
28
RSS/main.lua
Normal file
|
@ -0,0 +1,28 @@
|
|||
local LOG = require('log')
|
||||
local GRAPHICS = require('graphics')
|
||||
local FEEDS = require('feeds')
|
||||
|
||||
local REFRESH_INTERVAL = 8
|
||||
local DEBUG = true
|
||||
local MONITOR = assert(peripheral.find('monitor'), "No monitor detected")
|
||||
|
||||
local function main()
|
||||
LOG:Info("RSS System init")
|
||||
LOG:SetDebug(DEBUG)
|
||||
LOG:SetTarget(term)
|
||||
LOG:SetColours()
|
||||
GRAPHICS:SetTarget(MONITOR)
|
||||
GRAPHICS:SetColours()
|
||||
local err = FEEDS:Load()
|
||||
if err ~= nil then
|
||||
return
|
||||
end
|
||||
LOG:Success("System fully loaded, beginning RSS processing")
|
||||
|
||||
while true do
|
||||
GRAPHICS:Add(FEEDS:ParseLatest())
|
||||
sleep(REFRESH_INTERVAL)
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
259
RSS/slaxml.lua
Normal file
259
RSS/slaxml.lua
Normal file
|
@ -0,0 +1,259 @@
|
|||
--[=====================================================================[
|
||||
v0.8 Copyright © 2013-2018 Gavin Kistner <!@phrogz.net>; MIT Licensed
|
||||
See http://github.com/Phrogz/SLAXML for details.
|
||||
--]=====================================================================]
|
||||
local SLAXML = {
|
||||
VERSION = "0.8",
|
||||
_call = {
|
||||
pi = function(target,content)
|
||||
print(string.format("<?%s %s?>",target,content))
|
||||
end,
|
||||
comment = function(content)
|
||||
print(string.format("<!-- %s -->",content))
|
||||
end,
|
||||
startElement = function(name,nsURI,nsPrefix)
|
||||
io.write("<")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name)
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
print(">")
|
||||
end,
|
||||
attribute = function(name,value,nsURI,nsPrefix)
|
||||
io.write(' ')
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name,'=',string.format('%q',value))
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
io.write("\n")
|
||||
end,
|
||||
text = function(text,cdata)
|
||||
print(string.format(" %s: %q",cdata and 'cdata' or 'text',text))
|
||||
end,
|
||||
closeElement = function(name,nsURI,nsPrefix)
|
||||
io.write("</")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
print(name..">")
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
function SLAXML:parser(callbacks)
|
||||
return { _call=callbacks or self._call, parse=SLAXML.parse }
|
||||
end
|
||||
|
||||
function SLAXML:parse(xml,options)
|
||||
if not options then options = { stripWhitespace=false } end
|
||||
|
||||
-- Cache references for maximum speed
|
||||
local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat
|
||||
local first, last, match1, match2, match3, pos2, nsURI
|
||||
local unpack = unpack or table.unpack
|
||||
local pos = 1
|
||||
local state = "text"
|
||||
local textStart = 1
|
||||
local currentElement={}
|
||||
local currentAttributes={}
|
||||
local currentAttributeCt -- manually track length since the table is re-used
|
||||
local nsStack = {}
|
||||
local anyElement = false
|
||||
|
||||
local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
|
||||
local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string
|
||||
if decimal<128 then return char(decimal) end
|
||||
local charbytes = {}
|
||||
for bytes,vals in ipairs(utf8markers) do
|
||||
if decimal<=vals[1] then
|
||||
for b=bytes+1,2,-1 do
|
||||
local mod = decimal%64
|
||||
decimal = (decimal-mod)/64
|
||||
charbytes[b] = char(128+mod)
|
||||
end
|
||||
charbytes[1] = char(vals[2]+decimal)
|
||||
return concat(charbytes)
|
||||
end
|
||||
end
|
||||
end
|
||||
local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" }
|
||||
local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end
|
||||
local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end
|
||||
|
||||
local function finishText()
|
||||
if first>textStart and self._call.text then
|
||||
local text = sub(xml,textStart,first-1)
|
||||
if options.stripWhitespace then
|
||||
text = gsub(text,'^%s+','')
|
||||
text = gsub(text,'%s+$','')
|
||||
if #text==0 then text=nil end
|
||||
end
|
||||
if text then self._call.text(unescape(text),false) end
|
||||
end
|
||||
end
|
||||
|
||||
local function findPI()
|
||||
first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.pi then self._call.pi(match1,match2) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findComment()
|
||||
first, last, match1 = find( xml, '^<!%-%-(.-)%-%->', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.comment then self._call.comment(match1) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function nsForPrefix(prefix)
|
||||
if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl
|
||||
for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end
|
||||
error(("Cannot find namespace for prefix %s"):format(prefix))
|
||||
end
|
||||
|
||||
local function startElement()
|
||||
anyElement = true
|
||||
first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[2] = nil -- reset the nsURI, since this table is re-used
|
||||
currentElement[3] = nil -- reset the nsPrefix, since this table is re-used
|
||||
finishText()
|
||||
pos = last+1
|
||||
first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[1] = match2
|
||||
currentElement[3] = match1 -- Save the prefix for later resolution
|
||||
match1 = match2
|
||||
pos = last+1
|
||||
else
|
||||
currentElement[1] = match1
|
||||
for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end
|
||||
end
|
||||
currentAttributeCt = 0
|
||||
push(nsStack,{})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findAttribute()
|
||||
first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos )
|
||||
if first then
|
||||
pos2 = last+1
|
||||
first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
else
|
||||
first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
end
|
||||
end
|
||||
end
|
||||
if match1 and match2 then
|
||||
local currentAttribute = {match1,match2}
|
||||
local prefix,name = string.match(match1,'^([^:]+):([^:]+)$')
|
||||
if prefix then
|
||||
if prefix=='xmlns' then
|
||||
nsStack[#nsStack][name] = match2
|
||||
else
|
||||
currentAttribute[1] = name
|
||||
currentAttribute[4] = prefix
|
||||
end
|
||||
else
|
||||
if match1=='xmlns' then
|
||||
nsStack[#nsStack]['!'] = match2
|
||||
currentElement[2] = match2
|
||||
end
|
||||
end
|
||||
currentAttributeCt = currentAttributeCt + 1
|
||||
currentAttributes[currentAttributeCt] = currentAttribute
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findCDATA()
|
||||
first, last, match1 = find( xml, '^<!%[CDATA%[(.-)%]%]>', pos )
|
||||