:3
This commit is contained in:
parent
491112768c
commit
68ec37f994
66 changed files with 6591 additions and 10096 deletions
604
computer/28/youcube.lua
Normal file
604
computer/28/youcube.lua
Normal file
|
@ -0,0 +1,604 @@
|
|||
--[[
|
||||
_ _ ____ _ _ ____ _ _ ___ ____
|
||||
\_/ | | | | | | | |__] |___
|
||||
| |__| |__| |___ |__| |__] |___
|
||||
|
||||
Github Repository: https://github.com/Commandcracker/YouCube
|
||||
License: GPL-3.0
|
||||
]]
|
||||
|
||||
local _VERSION = "0.0.0-poc.1.1.2"
|
||||
|
||||
-- Libraries - OpenLibrarieLoader v1.0.1 --
|
||||
|
||||
--TODO: Optional libs:
|
||||
-- For something like a JSON lib that is only needed for older CC Versions or
|
||||
-- optional logging.lua support
|
||||
|
||||
local function is_lib(libs, lib)
|
||||
for i = 1, #libs do
|
||||
local value = libs[i]
|
||||
if value == lib or value .. ".lua" == lib then
|
||||
return true, value
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local libs = { "youcubeapi", "numberformatter", "semver", "argparse", "string_pack" }
|
||||
local lib_paths = { ".", "./lib", "./apis", "./modules", "/", "/lib", "/apis", "/modules" }
|
||||
|
||||
-- LevelOS Support
|
||||
if _G.lOS then
|
||||
lib_paths[#lib_paths + 1] = "/Program_Files/YouCube/lib"
|
||||
end
|
||||
|
||||
local function load_lib(lib)
|
||||
if require then
|
||||
return require(lib:gsub(".lua", ""))
|
||||
end
|
||||
return dofile(lib)
|
||||
end
|
||||
|
||||
for i_path = 1, #lib_paths do
|
||||
local path = lib_paths[i_path]
|
||||
if fs.exists(path) then
|
||||
local files = fs.list(path)
|
||||
for i_file = 1, #files do
|
||||
local found, lib = is_lib(libs, files[i_file])
|
||||
if found and lib ~= nil and libs[lib] == nil then
|
||||
libs[lib] = load_lib(path .. "/" .. files[i_file])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #libs do
|
||||
local lib = libs[i]
|
||||
if libs[lib] == nil then
|
||||
error(('Library "%s" not found.'):format(lib))
|
||||
end
|
||||
end
|
||||
|
||||
-- args --
|
||||
|
||||
local function get_program_name()
|
||||
if arg then
|
||||
return arg[0]
|
||||
end
|
||||
return fs.getName(shell.getRunningProgram()):gsub("[\\.].*$", "")
|
||||
end
|
||||
|
||||
-- stylua: ignore start
|
||||
|
||||
local parser = libs.argparse {
|
||||
help_max_width = ({ term.getSize() })[1],
|
||||
name = get_program_name()
|
||||
}
|
||||
:description "Official YouCube client for accessing media from services like YouTube"
|
||||
|
||||
parser:argument "URL"
|
||||
:args "*"
|
||||
:description "URL or search term."
|
||||
|
||||
parser:flag "-v" "--verbose"
|
||||
:description "Enables verbose output."
|
||||
:target "verbose"
|
||||
:action "store_true"
|
||||
|
||||
parser:option "-V" "--volume"
|
||||
:description "Sets the volume of the audio. A value from 0-100"
|
||||
:target "volume"
|
||||
|
||||
parser:option "-s" "--server"
|
||||
:description "The server that YC should use."
|
||||
:target "server"
|
||||
:args(1)
|
||||
|
||||
parser:flag "--nv" "--no-video"
|
||||
:description "Disables video."
|
||||
:target "no_video"
|
||||
:action "store_true"
|
||||
|
||||
parser:flag "--na" "--no-audio"
|
||||
:description "Disables audio."
|
||||
:target "no_audio"
|
||||
:action "store_true"
|
||||
|
||||
parser:flag "--sh" "--shuffle"
|
||||
:description "Shuffles audio before playing"
|
||||
:target "shuffle"
|
||||
:action "store_true"
|
||||
|
||||
parser:flag "-l" "--loop"
|
||||
:description "Loops the media."
|
||||
:target "loop"
|
||||
:action "store_true"
|
||||
|
||||
parser:flag "--lp" "--loop-playlist"
|
||||
:description "Loops the playlist."
|
||||
:target "loop_playlist"
|
||||
:action "store_true"
|
||||
|
||||
parser:option "--fps"
|
||||
:description "Force sanjuuni to use a specified frame rate"
|
||||
:target "force_fps"
|
||||
|
||||
-- stylua: ignore end
|
||||
|
||||
local args = parser:parse({ ... })
|
||||
|
||||
if args.force_fps then
|
||||
args.force_fps = tonumber(args.force_fps)
|
||||
end
|
||||
|
||||
if args.volume then
|
||||
args.volume = tonumber(args.volume)
|
||||
if args.volume == nil then
|
||||
parser:error("Volume must be a number")
|
||||
end
|
||||
|
||||
if args.volume > 100 then
|
||||
parser:error("Volume cant be over 100")
|
||||
end
|
||||
|
||||
if args.volume < 0 then
|
||||
parser:error("Volume cant be below 0")
|
||||
end
|
||||
args.volume = args.volume / 100
|
||||
end
|
||||
|
||||
if #args.URL > 0 then
|
||||
args.URL = table.concat(args.URL, " ")
|
||||
else
|
||||
args.URL = nil
|
||||
end
|
||||
|
||||
if args.no_video and args.no_audio then
|
||||
parser:error("Nothing will happen, when audio and video is disabled!")
|
||||
end
|
||||
|
||||
-- CraftOS-PC support --
|
||||
|
||||
if periphemu then
|
||||
periphemu.create("top", "speaker")
|
||||
-- Fuck the max websocket message police
|
||||
config.set("http_max_websocket_message", 2 ^ 30)
|
||||
end
|
||||
|
||||
local function get_audiodevices()
|
||||
local audiodevices = {}
|
||||
|
||||
local speakers = { peripheral.find("speaker") }
|
||||
for i = 1, #speakers do
|
||||
audiodevices[#audiodevices + 1] = libs.youcubeapi.Speaker.new(speakers[i])
|
||||
end
|
||||
|
||||
local tapes = { peripheral.find("tape_drive") }
|
||||
for i = 1, #tapes do
|
||||
audiodevices[#audiodevices + 1] = libs.youcubeapi.Tape.new(tapes[i])
|
||||
end
|
||||
|
||||
if #audiodevices == 0 then
|
||||
-- Disable audio when no audiodevice is found
|
||||
args.no_audio = true
|
||||
return audiodevices
|
||||
end
|
||||
|
||||
-- Validate audiodevices
|
||||
local last_error
|
||||
local valid_audiodevices = {}
|
||||
|
||||
for i = 1, #audiodevices do
|
||||
local audiodevice = audiodevices[i]
|
||||
local _error = audiodevice:validate()
|
||||
if _error == nil then
|
||||
valid_audiodevices[#valid_audiodevices + 1] = audiodevice
|
||||
else
|
||||
last_error = _error
|
||||
end
|
||||
end
|
||||
|
||||
if #valid_audiodevices == 0 then
|
||||
error(last_error)
|
||||
end
|
||||
|
||||
return valid_audiodevices
|
||||
end
|
||||
|
||||
-- main --
|
||||
|
||||
local youcubeapi = libs.youcubeapi.API.new()
|
||||
local audiodevices = get_audiodevices()
|
||||
|
||||
-- update check --
|
||||
|
||||
local function get_versions()
|
||||
local url = "https://raw.githubusercontent.com/CC-YouCube/installer/main/versions.json"
|
||||
|
||||
-- Check if the URL is valid
|
||||
local ok, err = http.checkURL(url)
|
||||
if not ok then
|
||||
printError("Invalid Update URL.", '"' .. url .. '" ', err)
|
||||
return
|
||||
end
|
||||
|
||||
local response, http_err = http.get(url, nil, true)
|
||||
if not response then
|
||||
printError('Failed to retreat data from update URL. "' .. url .. '" (' .. http_err .. ")")
|
||||
return
|
||||
end
|
||||
|
||||
local sResponse = response.readAll()
|
||||
response.close()
|
||||
|
||||
return textutils.unserialiseJSON(sResponse)
|
||||
end
|
||||
|
||||
local function write_colored(text, color)
|
||||
term.setTextColor(color)
|
||||
term.write(text)
|
||||
end
|
||||
|
||||
local function new_line()
|
||||
local w, h = term.getSize()
|
||||
local x, y = term.getCursorPos()
|
||||
if y + 1 <= h then
|
||||
term.setCursorPos(1, y + 1)
|
||||
else
|
||||
term.setCursorPos(1, h)
|
||||
term.scroll(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function write_outdated(current, latest)
|
||||
if libs.semver(current) ^ libs.semver(latest) then
|
||||
term.setTextColor(colors.yellow)
|
||||
else
|
||||
term.setTextColor(colors.red)
|
||||
end
|
||||
|
||||
term.write(current)
|
||||
write_colored(" -> ", colors.lightGray)
|
||||
write_colored(latest, colors.lime)
|
||||
term.setTextColor(colors.white)
|
||||
new_line()
|
||||
end
|
||||
|
||||
local function can_update(name, current, latest)
|
||||
if libs.semver(current) < libs.semver(latest) then
|
||||
term.write(name .. " ")
|
||||
write_outdated(current, latest)
|
||||
end
|
||||
end
|
||||
|
||||
local function update_checker()
|
||||
local versions = get_versions()
|
||||
if versions == nil then
|
||||
return
|
||||
end
|
||||
|
||||
can_update("youcube", _VERSION, versions.client.version)
|
||||
can_update("youcubeapi", libs.youcubeapi._VERSION, versions.client.libraries.youcubeapi.version)
|
||||
can_update("numberformatter", libs.numberformatter._VERSION, versions.client.libraries.numberformatter.version)
|
||||
can_update("semver", tostring(libs.semver._VERSION), versions.client.libraries.semver.version)
|
||||
can_update("argparse", libs.argparse.version, versions.client.libraries.argparse.version)
|
||||
|
||||
local handshake = youcubeapi:handshake()
|
||||
|
||||
if libs.semver(handshake.server.version) < libs.semver(versions.server.version) then
|
||||
print("Tell the server owner to update their server!")
|
||||
write_outdated(handshake.server.version, versions.server.version)
|
||||
end
|
||||
|
||||
if not libs.semver(libs.youcubeapi._API_VERSION) ^ libs.semver(handshake.api.version) then
|
||||
print("Client is not compatible with server")
|
||||
write_colored(libs.youcubeapi._API_VERSION, colors.red)
|
||||
write_colored(" ^ ", colors.lightGray)
|
||||
write_colored(handshake.api.version, colors.red)
|
||||
term.setTextColor(colors.white)
|
||||
new_line()
|
||||
end
|
||||
|
||||
if libs.semver(libs.youcubeapi._API_VERSION) < libs.semver(versions.api.version) then
|
||||
print("Your client is using an outdated API version")
|
||||
write_outdated(libs.youcubeapi._API_VERSION, versions.api.version)
|
||||
end
|
||||
|
||||
if libs.semver(handshake.api.version) < libs.semver(versions.api.version) then
|
||||
print("The server is using an outdated API version")
|
||||
write_outdated(libs.youcubeapi._API_VERSION, versions.api.version)
|
||||
end
|
||||
end
|
||||
|
||||
local function play_audio(buffer, title)
|
||||
for i = 1, #audiodevices do
|
||||
local audiodevice = audiodevices[i]
|
||||
audiodevice:reset()
|
||||
audiodevice:setLabel(title)
|
||||
audiodevice:setVolume(args.volume)
|
||||
end
|
||||
|
||||
while true do
|
||||
local chunk = buffer:next()
|
||||
|
||||
-- Adjust buffer size on first chunk
|
||||
if buffer.filler.chunkindex == 1 then
|
||||
buffer.size = math.ceil(1024 / (#chunk / 16))
|
||||
end
|
||||
|
||||
if chunk == "" then
|
||||
local play_functions = {}
|
||||
for i = 1, #audiodevices do
|
||||
local audiodevice = audiodevices[i]
|
||||
play_functions[#play_functions + 1] = function()
|
||||
audiodevice:play()
|
||||
end
|
||||
end
|
||||
|
||||
parallel.waitForAll(table.unpack(play_functions))
|
||||
return
|
||||
end
|
||||
|
||||
local write_functions = {}
|
||||
for i = 1, #audiodevices do
|
||||
local audiodevice = audiodevices[i]
|
||||
table.insert(write_functions, function()
|
||||
audiodevice:write(chunk)
|
||||
end)
|
||||
end
|
||||
|
||||
parallel.waitForAll(table.unpack(write_functions))
|
||||
end
|
||||
end
|
||||
|
||||
-- #region playback controll vars
|
||||
local back_buffer = {}
|
||||
local max_back = settings.get("youcube.max_back") or 32
|
||||
local queue = {}
|
||||
local restart = false
|
||||
-- #endregion
|
||||
|
||||
-- keys
|
||||
local skip_key = settings.get("youcube.keys.skip") or keys.d
|
||||
local restart_key = settings.get("youcube.keys.restart") or keys.r
|
||||
local back_key = settings.get("youcube.keys.back") or keys.a
|
||||
|
||||
local function play(url)
|
||||
restart = false
|
||||
print("Requesting media ...")
|
||||
|
||||
if not args.no_video then
|
||||
youcubeapi:request_media(url, term.getSize())
|
||||
else
|
||||
youcubeapi:request_media(url)
|
||||
end
|
||||
|
||||
local data
|
||||
local x, y = term.getCursorPos()
|
||||
|
||||
repeat
|
||||
data = youcubeapi:receive()
|
||||
if data.action == "status" then
|
||||
os.queueEvent("youcube:status", data)
|
||||
term.setCursorPos(x, y)
|
||||
term.clearLine()
|
||||
term.write("Status: ")
|
||||
write_colored(data.message, colors.green)
|
||||
term.setTextColor(colors.white)
|
||||
else
|
||||
new_line()
|
||||
end
|
||||
until data.action == "media"
|
||||
|
||||
if data.action == "error" then
|
||||
error(data.message)
|
||||
end
|
||||
|
||||
term.write("Playing: ")
|
||||
term.setTextColor(colors.lime)
|
||||
print(data.title)
|
||||
term.setTextColor(colors.white)
|
||||
|
||||
if data.like_count then
|
||||
print("Likes: " .. libs.numberformatter.compact(data.like_count))
|
||||
end
|
||||
|
||||
if data.view_count then
|
||||
print("Views: " .. libs.numberformatter.compact(data.view_count))
|
||||
end
|
||||
|
||||
if not args.no_video then
|
||||
-- wait, that the user can see the video info
|
||||
sleep(2)
|
||||
end
|
||||
|
||||
local video_buffer = libs.youcubeapi.Buffer.new(
|
||||
libs.youcubeapi.VideoFiller.new(youcubeapi, data.id, term.getSize()),
|
||||
60 -- Most videos run on 30 fps, so we store 2s of video.
|
||||
)
|
||||
|
||||
local audio_buffer = libs.youcubeapi.Buffer.new(
|
||||
libs.youcubeapi.AudioFiller.new(youcubeapi, data.id),
|
||||
--[[
|
||||
We want to buffer 1024 chunks.
|
||||
One chunks is 16 bits.
|
||||
The server (with default settings) sends 32 chunks at once.
|
||||
]]
|
||||
32
|
||||
)
|
||||
|
||||
if args.verbose then
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
term.write("[DEBUG MODE]")
|
||||
end
|
||||
|
||||
local function fill_buffers()
|
||||
while true do
|
||||
os.queueEvent("youcube:fill_buffers")
|
||||
|
||||
local event = os.pullEventRaw()
|
||||
|
||||
if event == "terminate" then
|
||||
libs.youcubeapi.reset_term()
|
||||
end
|
||||
|
||||
if not args.no_audio then
|
||||
audio_buffer:fill()
|
||||
end
|
||||
|
||||
if args.verbose then
|
||||
term.setCursorPos(1, ({ term.getSize() })[2])
|
||||
term.clearLine()
|
||||
term.write("Audio_Buffer: " .. #audio_buffer.buffer)
|
||||
end
|
||||
|
||||
if not args.no_video then
|
||||
video_buffer:fill()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _play_video()
|
||||
if not args.no_video then
|
||||
local string_unpack
|
||||
if not string.unpack then
|
||||
string_unpack = libs.string_pack.unpack
|
||||
end
|
||||
|
||||
os.queueEvent("youcube:vid_playing", data)
|
||||
libs.youcubeapi.play_vid(video_buffer, args.force_fps, string_unpack)
|
||||
os.queueEvent("youcube:vid_eof", data)
|
||||
end
|
||||
end
|
||||
|
||||
local function _play_audio()
|
||||
if not args.no_audio then
|
||||
os.queueEvent("youcube:audio_playing", data)
|
||||
play_audio(audio_buffer, data.title)
|
||||
os.queueEvent("youcube:audio_eof", data)
|
||||
end
|
||||
end
|
||||
|
||||
local function _play_media()
|
||||
os.queueEvent("youcube:playing")
|
||||
parallel.waitForAll(_play_video, _play_audio)
|
||||
end
|
||||
|
||||
local function _hotkey_handler()
|
||||
while true do
|
||||
local _, key = os.pullEvent("key")
|
||||
|
||||
if key == skip_key then
|
||||
back_buffer[#back_buffer + 1] = url --finished playing, push the value to the back buffer
|
||||
if #back_buffer > max_back then
|
||||
back_buffer[1] = nil --remove it from the front of the buffer
|
||||
end
|
||||
if not args.no_video then
|
||||
libs.youcubeapi.reset_term()
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
if key == restart_key then
|
||||
queue[#queue + 1] = url --add the current song to upcoming
|
||||
if not args.no_video then
|
||||
libs.youcubeapi.reset_term()
|
||||
end
|
||||
restart = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parallel.waitForAny(fill_buffers, _play_media, _hotkey_handler)
|
||||
|
||||
if data.playlist_videos then
|
||||
return data.playlist_videos
|
||||
end
|
||||
end
|
||||
|
||||
local function shuffle_playlist(playlist)
|
||||
local shuffled = {}
|
||||
for i = 1, #queue do
|
||||
local pos = math.random(1, #shuffled + 1)
|
||||
shuffled[pos] = queue[i]
|
||||
end
|
||||
return shuffled
|
||||
end
|
||||
|
||||
local function play_playlist(playlist)
|
||||
queue = playlist
|
||||
if args.shuffle then
|
||||
queue = shuffle_playlist(queue)
|
||||
end
|
||||
while #queue ~= 0 do
|
||||
local pl = table.remove(queue)
|
||||
|
||||
local function handle_back_hotkey()
|
||||
while true do
|
||||
local _, key = os.pullEvent("key")
|
||||
if key == back_key then
|
||||
queue[#queue + 1] = pl --add the current song to upcoming
|
||||
local prev = table.remove(back_buffer)
|
||||
if prev then --nil/false check
|
||||
queue[#queue + 1] = prev --add previous song to upcoming
|
||||
end
|
||||
if not args.no_video then
|
||||
libs.youcubeapi.reset_term()
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parallel.waitForAny(handle_back_hotkey, function()
|
||||
play(pl) --play the url
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local function main()
|
||||
youcubeapi:detect_bestest_server(args.server, args.verbose)
|
||||
pcall(update_checker)
|
||||
|
||||
if not args.URL then
|
||||
print("Enter Url or Search Term:")
|
||||
term.setTextColor(colors.lightGray)
|
||||
args.URL = read()
|
||||
term.setTextColor(colors.white)
|
||||
end
|
||||
|
||||
local playlist_videos = play(args.URL)
|
||||
|
||||
if args.loop == true then
|
||||
while true do
|
||||
play(args.URL)
|
||||
end
|
||||
end
|
||||
if playlist_videos then
|
||||
if args.loop_playlist == true then
|
||||
while true do
|
||||
if playlist_videos then
|
||||
play_playlist(playlist_videos)
|
||||
end
|
||||
end
|
||||
end
|
||||
play_playlist(playlist_videos)
|
||||
end
|
||||
|
||||
while restart do
|
||||
play(args.URL)
|
||||
end
|
||||
|
||||
youcubeapi.websocket.close()
|
||||
|
||||
if not args.no_video then
|
||||
libs.youcubeapi.reset_term()
|
||||
end
|
||||
|
||||
os.queueEvent("youcube:playback_ended")
|
||||
end
|
||||
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue