Compare commits

..

2 Commits

Author SHA1 Message Date
Kyle Yasuda
e35aeac422
cleanup/optimize code and squash some bugs (#12)
* cleanup/optimize code and squash some bugs

- change print_queue to use mp.assdraw
- change print_queue function to a toggle
- clean up duplicated code
- squash some bugs

* remove unused function

* make download async and update clipboard function to use mp command

- utilize mp.command_native and command_native_async
- download files in the background

* update default display_limit from 6 -> 10
2023-08-09 01:24:24 -07:00
ksyasuda
3a47cb0d0b
update luacheck workflow 2023-08-09 01:21:40 -07:00
4 changed files with 162 additions and 148 deletions

View File

@ -1,7 +1,7 @@
name: Luacheck name: Luacheck
on: [push, pull_request] on: push
jobs: jobs:
sile: luacheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@ -72,7 +72,7 @@ This script requires the following software to be installed on the system
- `browser - firefox`: The browser to use when opening a video or channel page - `browser - firefox`: The browser to use when opening a video or channel page
- `clipboard_command - xclip -o`: The command to use to get the contents of the clipboard - `clipboard_command - xclip -o`: The command to use to get the contents of the clipboard
- `cursor_icon - ➤`: The icon to use for the cursor - `cursor_icon - ➤`: The icon to use for the cursor
- `display_limit - 6`: The maximum amount of videos to show on the OSD at once - `display_limit - 10`: The maximum amount of videos to show on the OSD at once
- `download_directory - ~/videos/YouTube`: The directory to use when - `download_directory - ~/videos/YouTube`: The directory to use when
downloading a video downloading a video
- `download_quality 720p`: The maximum download quality - `download_quality 720p`: The maximum download quality

View File

@ -15,7 +15,7 @@ play_selected_video=ctrl+ENTER
browser=firefox browser=firefox
clipboard_command=xclip -o clipboard_command=xclip -o
cursor_icon=➤ cursor_icon=➤
display_limit=6 display_limit=10
download_directory=~/videos/YouTube download_directory=~/videos/YouTube
download_quality=720p download_quality=720p
downloader=curl downloader=curl

View File

@ -16,6 +16,7 @@
local mp = require 'mp' local mp = require 'mp'
mp.options = require 'mp.options' mp.options = require 'mp.options'
local utils = require 'mp.utils' local utils = require 'mp.utils'
local assdraw = require 'mp.assdraw'
local styleOn = mp.get_property("osd-ass-cc/0") local styleOn = mp.get_property("osd-ass-cc/0")
local styleOff = mp.get_property("osd-ass-cc/1") local styleOff = mp.get_property("osd-ass-cc/1")
@ -37,7 +38,7 @@ local options = {
browser = "firefox", browser = "firefox",
clipboard_command = "xclip -o", clipboard_command = "xclip -o",
cursor_icon = "", cursor_icon = "",
display_limit = 6, display_limit = 10,
download_directory = "~/videos/YouTube", download_directory = "~/videos/YouTube",
download_quality = "720p", download_quality = "720p",
downloader = "curl", downloader = "curl",
@ -51,6 +52,7 @@ local options = {
mp.options.read_options(options, "mpv-youtube-queue") mp.options.read_options(options, "mpv-youtube-queue")
-- STYLE {{{
local colors = { local colors = {
error = "676EFF", error = "676EFF",
selected = "F993BD", selected = "F993BD",
@ -79,6 +81,7 @@ local style = {
font = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size .. "{" .. font = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size .. "{" ..
sortoftransparent .. "}" sortoftransparent .. "}"
} }
-- }}}
local YouTubeQueue = {} local YouTubeQueue = {}
local video_queue = {} local video_queue = {}
@ -89,6 +92,16 @@ local selected_index = 1
local display_offset = 0 local display_offset = 0
local marked_index = nil local marked_index = nil
local current_video = nil local current_video = nil
local destroyer = nil
local timeout
local function destroy()
timeout:kill()
mp.set_osd_ass(0, 0, "")
destroyer = nil
end
timeout = mp.add_periodic_timer(5, destroy)
-- HELPERS {{{ -- HELPERS {{{
@ -108,6 +121,7 @@ local function sleep(n) os.execute("sleep " .. tonumber(n)) end
local function print_osd_message(message, duration, s) local function print_osd_message(message, duration, s)
if s == style.error and not options.show_errors then return end if s == style.error and not options.show_errors then return end
destroy()
if s == nil then s = style.font .. "{" .. notransparent .. "}" end if s == nil then s = style.font .. "{" .. notransparent .. "}" end
if duration == nil then duration = MSG_DURATION end if duration == nil then duration = MSG_DURATION end
mp.osd_message(styleOn .. s .. message .. style.reset .. styleOff .. "\n", mp.osd_message(styleOn .. s .. message .. style.reset .. styleOff .. "\n",
@ -117,9 +131,7 @@ end
-- returns true if the provided path exists and is a file -- returns true if the provided path exists and is a file
local function is_file(filepath) local function is_file(filepath)
local result = utils.file_info(filepath) local result = utils.file_info(filepath)
if result == nil then if result == nil then return false end
return false
end
return result.is_file return result.is_file
end end
@ -128,16 +140,6 @@ local function split_path(filepath)
if is_file(filepath) then return utils.split_path(filepath) end if is_file(filepath) then return utils.split_path(filepath) end
end end
local function print_current_video()
local current = YouTubeQueue.get_current_video()
if is_file(current.video_url) then
print_osd_message("Playing: " .. current.video_name, 3)
else
print_osd_message("Playing: " .. current.video_name .. ' by ' ..
current.channel_name, 3)
end
end
local function expanduser(path) local function expanduser(path)
-- remove trailing slash if it exists -- remove trailing slash if it exists
if string.sub(path, -1) == "/" then path = string.sub(path, 1, -2) end if string.sub(path, -1) == "/" then path = string.sub(path, 1, -2) end
@ -166,16 +168,22 @@ local function open_channel_in_browser()
open_url_in_browser(YouTubeQueue.get_current_video().channel_url) open_url_in_browser(YouTubeQueue.get_current_video().channel_url)
end end
-- local function is_valid_ytdlp_url(url) -- local function _print_internal_playlist()
-- local command = 'yt-dlp --simulate \'' .. url .. '\' >/dev/null 2>&1' -- local count = mp.get_property_number("playlist-count")
-- local handle = io.popen(command .. "; echo $?") -- print("Playlist contents:")
-- if handle == nil then return false end -- for i = 0, count - 1 do
-- local result = handle:read("*a") -- local uri = mp.get_property(string.format("playlist/%d/filename", i))
-- if result == nil then return false end -- print(string.format("%d: %s", i, uri))
-- handle:close() -- end
-- return result:gsub("%s+$", "") == "0"
-- end -- end
local function toggle_print()
if destroyer ~= nil then
destroyer()
else
YouTubeQueue.print_queue()
end
end
-- }}} -- }}}
-- QUEUE GETTERS AND SETTERS {{{ -- QUEUE GETTERS AND SETTERS {{{
@ -203,15 +211,21 @@ end
-- returns the content of the clipboard -- returns the content of the clipboard
function YouTubeQueue.get_clipboard_content() function YouTubeQueue.get_clipboard_content()
local handle = io.popen(options.clipboard_command) local command, args = options.clipboard_command:match("(%S+)%s+(%S+)")
if handle == nil then local res = mp.command_native({
print_osd_message("Error getting clipboard content", MSG_DURATION, name = "subprocess",
playback_only = false,
capture_stdout = true,
args = { command, args }
})
if res.status ~= 0 then
print_osd_message("Failed to get clipboard content", MSG_DURATION,
style.error) style.error)
return nil return nil
end end
local result = handle:read("*a")
handle:close() return res.stdout
return result
end end
function YouTubeQueue.get_video_info(url) function YouTubeQueue.get_video_info(url)
@ -236,28 +250,42 @@ function YouTubeQueue.get_video_info(url)
return channel_url, channel_name, video_name return channel_url, channel_name, video_name
end end
function YouTubeQueue.print_current_video()
destroy()
local current = current_video
if is_file(current.video_url) then
print_osd_message("Playing: " .. current.video_name, 3)
else
print_osd_message("Playing: " .. current.video_name .. ' by ' ..
current.channel_name, 3)
end
end
-- }}} -- }}}
-- QUEUE FUNCTIONS {{{ -- QUEUE FUNCTIONS {{{
-- Function to get the next video in the queue
-- Returns nil if there are no videos in the queue
function YouTubeQueue.next_in_queue()
if index < #video_queue then
index = index + 1
selected_index = index
current_video = video_queue[index]
return current_video
end
end
function YouTubeQueue.prev_in_queue() -- Function to set the next or previous video in the queue as the current video
if index > 1 then -- direction can be "NEXT" or "PREV". If nil, "next" is assumed
index = index - 1 -- Returns nil if there are no more videos in the queue
function YouTubeQueue.set_video(direction)
local amt
direction = string.upper(direction)
if (direction == "NEXT" or direction == nil) then
amt = 1
elseif (direction == "PREV" or direction == "PREVIOUS") then
amt = -1
else
print_osd_message("Invalid direction: " .. direction, MSG_DURATION,
style.error)
return nil
end
if index + amt > #video_queue or index + amt == 0 then return nil end
index = index + amt
selected_index = index selected_index = index
current_video = video_queue[index] current_video = video_queue[index]
return current_video return current_video
end end
end
function YouTubeQueue.is_in_queue(url) function YouTubeQueue.is_in_queue(url)
for _, v in ipairs(video_queue) do for _, v in ipairs(video_queue) do
@ -309,11 +337,14 @@ function YouTubeQueue.reorder_queue(from_index, to_index)
table.remove(video_queue, from_index) table.remove(video_queue, from_index)
table.insert(video_queue, to_index, temp_video) table.insert(video_queue, to_index, temp_video)
-- Swap the videos between the two provided indices in the MPV playlist -- swap the videos in the mpv playlist
mp.commandv("playlist-move", from_index - 1, to_index - 1) -- playlist-move is 0-indexed and works the opposite of what is expected
-- ex: playlist-move 1 2 will move the video at index 2 to index 1
-- Redraw the queue after reordering mp.commandv("loadfile", video_queue[to_index].video_url, "append")
YouTubeQueue.print_queue() mp.commandv("playlist-move", #video_queue, to_index - 1)
mp.commandv("playlist-move", to_index - 1, from_index - 1)
mp.commandv("playlist-move", from_index - 1, #video_queue)
mp.commandv("playlist-remove", #video_queue)
else else
print_osd_message("Invalid indices for reordering. No changes made.", print_osd_message("Invalid indices for reordering. No changes made.",
MSG_DURATION, style.error) MSG_DURATION, style.error)
@ -321,36 +352,38 @@ function YouTubeQueue.reorder_queue(from_index, to_index)
end end
function YouTubeQueue.print_queue(duration) function YouTubeQueue.print_queue(duration)
timeout:kill()
timeout:resume()
local ass = assdraw.ass_new()
local current_index = index local current_index = index
if duration == nil then duration = 3 end
if #video_queue > 0 then if #video_queue > 0 then
local start_index = math.max(1, selected_index - display_limit / 2) local start_index = math.max(1, selected_index - display_limit / 2)
local end_index = local end_index =
math.min(#video_queue, start_index + display_limit - 1) math.min(#video_queue, start_index + display_limit - 1)
display_offset = start_index - 1 display_offset = start_index - 1
local message = ass:append(
styleOn .. style.header .. "MPV-YOUTUBE-QUEUE{\\u0\\b0}" .. style.header .. "MPV-YOUTUBE-QUEUE{\\u0\\b0}" .. style.reset ..
style.reset .. style.font .. "\n" style.font .. "\n")
local message
for i = start_index, end_index do for i = start_index, end_index do
local prefix = (i == selected_index) and style.cursor .. local prefix = (i == selected_index) and style.cursor ..
options.cursor_icon .. " " .. style.reset or options.cursor_icon .. " " .. style.reset or
" " " "
if i == current_index and i == selected_index then if i == current_index and i == selected_index then
message = message = prefix .. style.hover_selected .. i .. ". " ..
message .. prefix .. style.hover_selected .. i .. ". " ..
video_queue[i].video_name .. " - (" .. video_queue[i].video_name .. " - (" ..
video_queue[i].channel_name .. ")" .. style.reset video_queue[i].channel_name .. ")" .. style.reset
elseif i == current_index then elseif i == current_index then
message = message .. prefix .. style.selected .. i .. ". " .. message = prefix .. style.selected .. i .. ". " ..
video_queue[i].video_name .. " - (" .. video_queue[i].video_name .. " - (" ..
video_queue[i].channel_name .. ")" .. style.reset video_queue[i].channel_name .. ")" .. style.reset
elseif i == selected_index then elseif i == selected_index then
message = message .. prefix .. style.hover .. i .. ". " .. message = prefix .. style.hover .. i .. ". " ..
video_queue[i].video_name .. " - (" .. video_queue[i].video_name .. " - (" ..
video_queue[i].channel_name .. ")" .. style.reset video_queue[i].channel_name .. ")" .. style.reset
else else
message = message .. prefix .. style.reset .. i .. ". " .. message = prefix .. style.reset .. i .. ". " ..
video_queue[i].video_name .. " - (" .. video_queue[i].video_name .. " - (" ..
video_queue[i].channel_name .. ")" .. style.reset video_queue[i].channel_name .. ")" .. style.reset
end end
@ -361,49 +394,44 @@ function YouTubeQueue.print_queue(duration)
else else
message = message .. "\n" message = message .. "\n"
end end
ass:append(style.font .. message)
end
mp.set_osd_ass(0, 0, ass.text)
if duration ~= nil then
mp.add_timeout(duration, function() destroy() end)
end end
message = message .. styleOff
mp.osd_message(message, duration)
else else
print_osd_message("No videos in the queue or history.", duration, print_osd_message("No videos in the queue or history.", duration,
style.error) style.error)
end end
destroyer = destroy
end end
function YouTubeQueue.move_cursor_up(amt) function YouTubeQueue.move_cursor(amt)
timeout:kill()
timeout:resume()
selected_index = selected_index - amt selected_index = selected_index - amt
if selected_index < 1 then if selected_index < 1 then
selected_index = 1 selected_index = 1
elseif selected_index > #video_queue then elseif selected_index > #video_queue then
selected_index = #video_queue selected_index = #video_queue
end end
if selected_index > 1 and selected_index < display_offset + 1 then if amt == 1 and selected_index > 1 and selected_index < display_offset + 1 then
display_offset = display_offset - math.abs(selected_index - amt) display_offset = display_offset - math.abs(selected_index - amt)
end elseif amt == -1 and selected_index < #video_queue and selected_index >
YouTubeQueue.print_queue(MSG_DURATION) display_offset + display_limit then
end
function YouTubeQueue.move_cursor_down(amt)
selected_index = selected_index + amt
if selected_index < 1 then
selected_index = 1
elseif selected_index > #video_queue then
selected_index = #video_queue
end
if selected_index < #video_queue and selected_index > display_offset + display_limit then
display_offset = display_offset + math.abs(selected_index - amt) display_offset = display_offset + math.abs(selected_index - amt)
end end
YouTubeQueue.print_queue(MSG_DURATION) YouTubeQueue.print_queue()
end end
function YouTubeQueue.play_video_at(idx) function YouTubeQueue.play_video_at(idx)
local queue = YouTubeQueue.get_video_queue() if idx <= 0 or idx > #video_queue then
if idx <= 0 or idx > #queue then
print_osd_message("Invalid video index", MSG_DURATION, style.error) print_osd_message("Invalid video index", MSG_DURATION, style.error)
return nil return nil
end end
YouTubeQueue.set_current_index(idx) index = idx
selected_index = index selected_index = idx
mp.set_property_number("playlist-pos", index - 1) -- zero-based index mp.set_property_number("playlist-pos", index - 1) -- zero-based index
return current_video return current_video
end end
@ -411,32 +439,33 @@ end
function YouTubeQueue.play_selected_video() function YouTubeQueue.play_selected_video()
-- local current_index = YouTubeQueue.get_current_index() -- local current_index = YouTubeQueue.get_current_index()
YouTubeQueue.play_video_at(selected_index) YouTubeQueue.play_video_at(selected_index)
YouTubeQueue.print_queue(MSG_DURATION - 0.5) YouTubeQueue.print_current_video()
sleep(MSG_DURATION)
print_current_video()
end end
-- play the next video in the queue -- play the next video in the queue
function YouTubeQueue.play_next_in_queue() function YouTubeQueue.play_video(direction)
local next_video = YouTubeQueue.next_in_queue() direction = string.upper(direction)
if next_video == nil then local video = YouTubeQueue.set_video(direction)
print_osd_message("No more videos in the queue.", MSG_DURATION, if video == nil then
style.error) print_osd_message("No video available.", MSG_DURATION, style.error)
return return
end end
local current_index = YouTubeQueue.get_current_index() current_video = video
selected_index = index
-- if the current video is not the first in the queue, then play the video -- if the current video is not the first in the queue, then play the video
-- else, check if the video is playing and if not play the video with replace -- else, check if the video is playing and if not play the video with replace
if YouTubeQueue.size() > 1 then if direction == "NEXT" and #video_queue > 1 then
mp.set_property_number("playlist-pos", current_index - 1) YouTubeQueue.play_video_at(index)
else elseif direction == "NEXT" and #video_queue == 1 then
local state = mp.get_property("core-idle") local state = mp.get_property("core-idle")
-- yes if the video is loaded but not currently playing
if state == "yes" then if state == "yes" then
mp.commandv("loadfile", next_video.video_url, "replace") mp.commandv("loadfile", video.video_url, "replace")
end end
elseif direction == "PREV" or direction == "PREVIOUS" then
mp.set_property_number("playlist-pos", index - 1)
end end
print_current_video() YouTubeQueue.print_current_video()
selected_index = current_index
sleep(MSG_DURATION) sleep(MSG_DURATION)
end end
@ -492,60 +521,43 @@ function YouTubeQueue.add_to_queue(url, update_internal_playlist)
-- if the queue was empty, start playing the video -- if the queue was empty, start playing the video
-- otherwise, add the video to the playlist -- otherwise, add the video to the playlist
if not YouTubeQueue.get_current_video() then if not YouTubeQueue.get_current_video() then
YouTubeQueue.play_next_in_queue() YouTubeQueue.play_video("NEXT")
elseif update_internal_playlist == 0 then elseif update_internal_playlist == 0 then
mp.commandv("loadfile", url, "append-play") mp.commandv("loadfile", url, "append-play")
end end
print_osd_message("Added " .. video_name .. " to queue.", MSG_DURATION) print_osd_message("Added " .. video_name .. " to queue.", MSG_DURATION)
end end
-- play the previous video in the queue
function YouTubeQueue.play_previous_video()
local previous_video = YouTubeQueue.prev_in_queue()
if previous_video == nil then
print_osd_message("No previous video available.", MSG_DURATION,
style.error)
return
end
local current_index = YouTubeQueue.get_current_index()
mp.set_property_number("playlist-pos", current_index - 1)
selected_index = current_index
print_current_video()
sleep(MSG_DURATION)
end
function YouTubeQueue.download_video_at(idx) function YouTubeQueue.download_video_at(idx)
local o = options local o = options
local v = video_queue[idx] local v = video_queue[idx]
local q = o.download_quality:sub(1, -2) local q = o.download_quality:sub(1, -2)
local dl_dir = expanduser(o.download_directory) local dl_dir = expanduser(o.download_directory)
local command = 'yt-dlp -f \'bestvideo[height<=' .. q .. '][ext=' ..
options.ytdlp_file_format .. ']+bestaudio/best[height<=' ..
q .. ']/bestvideo[height<=' .. q ..
']+bestaudio/best[height<=' .. q .. ']\' -o "' .. dl_dir ..
"/" .. options.ytdlp_output_template ..
'" --downloader ' .. o.downloader .. ' ' .. v.video_url
print_osd_message("Downloading " .. v.video_name .. "...", MSG_DURATION)
-- Run the download command -- Run the download command
local handle = io.popen(command) -- local handle = io.popen(command)
if handle == nil then mp.command_native_async({
print_osd_message("Error starting download.", MSG_DURATION, style.error) name = "subprocess",
return capture_stderr = true,
end detach = true,
print_osd_message("Starting download for " .. v.video_name, MSG_DURATION) args = {
local result = handle:read("*a") "yt-dlp", "-f",
handle:close() "bestvideo[height<=" .. q .. "][ext=" .. options.ytdlp_file_format ..
if result == nil then "]+bestaudio/best[height<=" .. q .. "]/bestvideo[height<=" .. q ..
print_osd_message("Error starting download.", MSG_DURATION, style.error) "]+bestaudio/best[height<=" .. q .. "]", "-o",
return dl_dir .. "/" .. options.ytdlp_output_template, "--downloader",
end o.downloader, "--", v.video_url
}
if result then }, function(success, _, err)
print_osd_message("Finished downloading " .. v.video_name, MSG_DURATION) if success then
print_osd_message("Finished downloading " .. v.video_name .. ".",
MSG_DURATION)
else else
print_osd_message("Error downloading " .. v.video_name, MSG_DURATION, print_osd_message("Error downloading " .. v.video_name .. ": " ..
style.error) err, MSG_DURATION, style.error)
end end
end)
end end
function YouTubeQueue.download_current_video() function YouTubeQueue.download_current_video()
@ -566,7 +578,7 @@ function YouTubeQueue.download_selected_video()
print_osd_message("No video to download.", MSG_DURATION, style.error) print_osd_message("No video to download.", MSG_DURATION, style.error)
return return
end end
if is_file(YouTubeQueue.get_video_at(selected_index)) then if is_file(YouTubeQueue.get_video_at(selected_index).video_name) then
print_osd_message("Current video is a local file... doing nothing.", print_osd_message("Current video is a local file... doing nothing.",
MSG_DURATION, style.error) MSG_DURATION, style.error)
return return
@ -619,20 +631,22 @@ end
mp.add_key_binding(options.add_to_queue, "add_to_queue", mp.add_key_binding(options.add_to_queue, "add_to_queue",
YouTubeQueue.add_to_queue) YouTubeQueue.add_to_queue)
mp.add_key_binding(options.play_next_in_queue, "play_next_in_queue", mp.add_key_binding(options.play_next_in_queue, "play_next_in_queue",
YouTubeQueue.play_next_in_queue) function() YouTubeQueue.play_video("NEXT") end)
mp.add_key_binding(options.play_previous_in_queue, "play_previous_video", mp.add_key_binding(options.play_previous_in_queue, "play_prev_in_queue",
YouTubeQueue.play_previous_video) function() YouTubeQueue.play_video("PREV") end)
mp.add_key_binding(options.print_queue, "print_queue", YouTubeQueue.print_queue) mp.add_key_binding(options.print_queue, "print_queue", toggle_print)
mp.add_key_binding(options.move_cursor_up, "move_cursor_up", mp.add_key_binding(options.move_cursor_up, "move_cursor_up",
function() YouTubeQueue.move_cursor_up(1) end, { repeatable = true }) function() YouTubeQueue.move_cursor(1) end,
{ repeatable = true })
mp.add_key_binding(options.move_cursor_down, "move_cursor_down", mp.add_key_binding(options.move_cursor_down, "move_cursor_down",
function() YouTubeQueue.move_cursor_down(1) end, { repeatable = true }) function() YouTubeQueue.move_cursor(-1) end,
{ repeatable = true })
mp.add_key_binding(options.play_selected_video, "play_selected_video", mp.add_key_binding(options.play_selected_video, "play_selected_video",
YouTubeQueue.play_selected_video) YouTubeQueue.play_selected_video)
mp.add_key_binding(options.open_video_in_browser, "open_video_in_browser", mp.add_key_binding(options.open_video_in_browser, "open_video_in_browser",
open_video_in_browser) open_video_in_browser)
mp.add_key_binding(options.print_current_video, "print_current_video", mp.add_key_binding(options.print_current_video, "print_current_video",
print_current_video) YouTubeQueue.print_current_video)
mp.add_key_binding(options.open_channel_in_browser, "open_channel_in_browser", mp.add_key_binding(options.open_channel_in_browser, "open_channel_in_browser",
open_channel_in_browser) open_channel_in_browser)
mp.add_key_binding(options.download_current_video, "download_current_video", mp.add_key_binding(options.download_current_video, "download_current_video",