trainworld_computercraft/computer/28/youcube.lua
2025-07-03 01:23:46 +02:00

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()