604 lines
16 KiB
Lua
604 lines
16 KiB
Lua
--[[
|
|
_ _ ____ _ _ ____ _ _ ___ ____
|
|
\_/ | | | | | | | |__] |___
|
|
| |__| |__| |___ |__| |__] |___
|
|
|
|
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()
|