diff --git a/README.md b/README.md
index e791ab7..64f49ae 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-A Lua script that implements the YouTube 'Add to Queue' functionality for mpv
+A Lua script that replicates and extends the YouTube "Add to Queue" feature for mpv
@@ -10,38 +10,28 @@ A Lua script that implements the YouTube 'Add to Queue' functionality for mpv
## Features
-- Add videos to a queue from the clipboard
- - Works with links from any site
- [supported by yt-dlp](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md "yd-dlp supported sites page")
-- An interactive menu to show the queue, to select a video to play, or to edit the order of the queue
-- Customizable keybindings to interact with the currrently playing video and the
- queue
-- Open the URL or channel page of the currently playing video in a new browser tab
-- Download a video in the queue using yt-dlp
-- Customizable download options
-- Integrates with the internal mpv playlist
-
-## Notes
-
-- This script uses the Linux `xclip` utility to read from the clipboard.
- If you're on macOS or Windows, you'll need to adjust the `clipboard_command`
- config variable in [mpv-youtube-queue.conf](./mpv-youtube-queue.conf)
-- When adding videos to the queue, the script fetches the video name using
- `yt-dlp`. Ensure you have `yt-dlp` installed and in your PATH.
+- **Interactive Queue Management:** A menu-driven interface for adding, removing, and rearranging videos in your queue
+- **yt-dlp Integration:** Works with any link supported by [yt-dlp](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md "yd-dlp supported sites page") and supports downloading a supported video in the queue
+- **Internal Playlist Integration:** Seamlessly integrates with mpv's internal playlist for a unified playback experience
+- **Customizable Keybindings:** Assign your preferred hotkeys to interact with the currently playing video and queue
## Requirements
This script requires the following software to be installed on the system
-- [xclip](https://github.com/astrand/xclip)
+- One of [xclip](https://github.com/astrand/xclip), [wl-clipboard](https://github.com/bugaevc/wl-clipboard), or any command-line utility that can paste from the system clipboard
+ - Windows users can utilize `Get-Clipboard` from powershell by setting the `clipboard_command` in `mpv-youtube-queue.conf` file to the following: `clipboard_command=powershell -command Get-Clipboard`
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
## Installation
-- Copy the `mpv-youtube-queue.lua` script to your `~~/scripts` directory
- (`~/.config/mpv` on Linux)
-- Optionally copy the `mpv-youtube-queue.conf` to the `~~/script-opts` directory
- to customize the script configuration as described in the next section
+- Copy `mpv-youtube-queue.lua` script to your `~~/scripts` directory
+ - `~/.config/mpv/scripts` on Linux
+ - `%APPDATA%\mpv\scripts` on Windows
+- Optionally copy `mpv-youtube-queue.conf` to the `~~/script-opts` directory
+ - `~/.config/mpv/script-opts` on Linux
+ - `%APPDATA%\mpv\script-opts` on Windows
+ to customize the script configuration as described in the next section
## Configuration
@@ -67,7 +57,7 @@ This script requires the following software to be installed on the system
- `play_selected_video - ctrl+ENTER`: Play the currently selected video in
the queue
-### Default Option
+### Default Options
- `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
diff --git a/mpv-youtube-queue.conf b/mpv-youtube-queue.conf
index e7418a1..18fb9b4 100644
--- a/mpv-youtube-queue.conf
+++ b/mpv-youtube-queue.conf
@@ -19,7 +19,7 @@ display_limit=10
download_directory=~/videos/YouTube
download_quality=720p
downloader=curl
-font_name=JetBrains Mono
+font_name=JetBrainsMono
font_size=12
marked_icon=⇅
menu_timeout=5
diff --git a/mpv-youtube-queue.lua b/mpv-youtube-queue.lua
index f1fe06b..c22491a 100644
--- a/mpv-youtube-queue.lua
+++ b/mpv-youtube-queue.lua
@@ -31,55 +31,55 @@ local destroyer = nil
local timeout
local options = {
- add_to_queue = "ctrl+a",
- download_current_video = "ctrl+d",
- download_selected_video = "ctrl+D",
- move_cursor_down = "ctrl+j",
- move_cursor_up = "ctrl+k",
- move_video = "ctrl+m",
- play_next_in_queue = "ctrl+n",
- open_video_in_browser = "ctrl+o",
- open_channel_in_browser = "ctrl+O",
- play_previous_in_queue = "ctrl+p",
- print_current_video = "ctrl+P",
- print_queue = "ctrl+q",
- remove_from_queue = "ctrl+x",
- play_selected_video = "ctrl+ENTER",
- browser = "firefox",
- clipboard_command = "xclip -o",
- cursor_icon = "➤",
- display_limit = 10,
- download_directory = "~/videos/YouTube",
- download_quality = "720p",
- downloader = "curl",
- font_name = "JetBrains Mono",
- font_size = 12,
- marked_icon = "⇅",
- menu_timeout = 5,
- show_errors = true,
- ytdlp_file_format = "mp4",
- ytdlp_output_template = "%(uploader)s/%(title)s.%(ext)s"
+ add_to_queue = "ctrl+a",
+ download_current_video = "ctrl+d",
+ download_selected_video = "ctrl+D",
+ move_cursor_down = "ctrl+j",
+ move_cursor_up = "ctrl+k",
+ move_video = "ctrl+m",
+ play_next_in_queue = "ctrl+n",
+ open_video_in_browser = "ctrl+o",
+ open_channel_in_browser = "ctrl+O",
+ play_previous_in_queue = "ctrl+p",
+ print_current_video = "ctrl+P",
+ print_queue = "ctrl+q",
+ remove_from_queue = "ctrl+x",
+ play_selected_video = "ctrl+ENTER",
+ browser = "firefox",
+ clipboard_command = "xclip -o",
+ cursor_icon = "➤",
+ display_limit = 10,
+ download_directory = "~/videos/YouTube",
+ download_quality = "720p",
+ downloader = "curl",
+ font_name = "JetBrains Mono",
+ font_size = 12,
+ marked_icon = "⇅",
+ menu_timeout = 5,
+ show_errors = true,
+ ytdlp_file_format = "mp4",
+ ytdlp_output_template = "%(uploader)s/%(title)s.%(ext)s"
}
mp.options.read_options(options, "mpv-youtube-queue")
local function destroy()
- timeout:kill()
- mp.set_osd_ass(0, 0, "")
- destroyer = nil
+ timeout:kill()
+ mp.set_osd_ass(0, 0, "")
+ destroyer = nil
end
timeout = mp.add_periodic_timer(options.menu_timeout, destroy)
-- STYLE {{{
local colors = {
- error = "676EFF",
- selected = "F993BD",
- hover_selected = "FAA9CA",
- cursor = "FDE98B",
- header = "8CFAF1",
- hover = "F2F8F8",
- text = "BFBFBF",
- marked = "C679FF"
+ error = "676EFF",
+ selected = "F993BD",
+ hover_selected = "FAA9CA",
+ cursor = "FDE98B",
+ header = "8CFAF1",
+ hover = "F2F8F8",
+ text = "BFBFBF",
+ marked = "C679FF"
}
local notransparent = "\\alpha&H00&"
@@ -87,28 +87,28 @@ local semitransparent = "\\alpha&H40&"
local sortoftransparent = "\\alpha&H59&"
local style = {
- error = "{\\c&" .. colors.error .. "&" .. notransparent .. "}",
- selected = "{\\c&" .. colors.selected .. "&" .. semitransparent .. "}",
- hover_selected = "{\\c&" .. colors.hover_selected .. "&\\alpha&H33&}",
- cursor = "{\\c&" .. colors.cursor .. "&" .. notransparent .. "}",
- marked = "{\\c&" .. colors.marked .. "&" .. notransparent .. "}",
- reset = "{\\c&" .. colors.text .. "&" .. sortoftransparent .. "}",
- header = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size * 1.5 ..
- "\\u1\\b1\\c&" .. colors.header .. "&" .. notransparent .. "}",
- hover = "{\\c&" .. colors.hover .. "&" .. semitransparent .. "}",
- font = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size .. "{" ..
- sortoftransparent .. "}"
+ error = "{\\c&" .. colors.error .. "&" .. notransparent .. "}",
+ selected = "{\\c&" .. colors.selected .. "&" .. semitransparent .. "}",
+ hover_selected = "{\\c&" .. colors.hover_selected .. "&\\alpha&H33&}",
+ cursor = "{\\c&" .. colors.cursor .. "&" .. notransparent .. "}",
+ marked = "{\\c&" .. colors.marked .. "&" .. notransparent .. "}",
+ reset = "{\\c&" .. colors.text .. "&" .. sortoftransparent .. "}",
+ header = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size * 1.5 ..
+ "\\u1\\b1\\c&" .. colors.header .. "&" .. notransparent .. "}",
+ hover = "{\\c&" .. colors.hover .. "&" .. semitransparent .. "}",
+ font = "{\\fn" .. options.font_name .. "\\fs" .. options.font_size .. "{" ..
+ sortoftransparent .. "}"
}
-- }}}
-- HELPERS {{{
-- surround string with single quotes if it does not already have them
local function surround_with_quotes(s)
- if string.sub(s, 0, 1) == "'" and string.sub(s, -1) == "'" then
- return s
- else
- return "'" .. s .. "'"
- end
+ if string.sub(s, 0, 1) == "'" and string.sub(s, -1) == "'" then
+ return s
+ else
+ return "'" .. s .. "'"
+ end
end
local function remove_quotes(s) return string.gsub(s, "'", "") end
@@ -117,136 +117,168 @@ local function remove_quotes(s) return string.gsub(s, "'", "") end
local function sleep(n) os.execute("sleep " .. tonumber(n)) end
local function print_osd_message(message, duration, s)
- if s == style.error and not options.show_errors then return end
- destroy()
- if s == nil then s = style.font .. "{" .. notransparent .. "}" end
- if duration == nil then duration = MSG_DURATION end
- mp.osd_message(styleOn .. s .. message .. style.reset .. styleOff .. "\n",
- duration)
+ if s == style.error and not options.show_errors then return end
+ destroy()
+ if s == nil then s = style.font .. "{" .. notransparent .. "}" end
+ if duration == nil then duration = MSG_DURATION end
+ mp.osd_message(styleOn .. s .. message .. style.reset .. styleOff .. "\n",
+ duration)
end
-- returns true if the provided path exists and is a file
local function is_file(filepath)
- local result = utils.file_info(filepath)
- if result == nil then return false end
- return result.is_file
+ local result = utils.file_info(filepath)
+ if result == nil then return false end
+ return result.is_file
end
-- returns the filename given a path (e.g. /home/user/file.txt -> file.txt)
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
local function expanduser(path)
- -- remove trailing slash if it exists
- if string.sub(path, -1) == "/" then path = string.sub(path, 1, -2) end
- if path:sub(1, 1) == "~" then
- local home = os.getenv("HOME")
- if home then
- return home .. path:sub(2)
- else
- return path
- end
- else
- return path
- end
+ -- remove trailing slash if it exists
+ if string.sub(path, -1) == "/" then path = string.sub(path, 1, -2) end
+ if path:sub(1, 1) == "~" then
+ local home = os.getenv("HOME")
+ if home then
+ return home .. path:sub(2)
+ else
+ return path
+ end
+ else
+ return path
+ end
end
local function open_url_in_browser(url)
- local command = options.browser .. " " .. surround_with_quotes(url)
- os.execute(command)
+ local command = options.browser .. " " .. surround_with_quotes(url)
+ os.execute(command)
end
local function open_video_in_browser()
- open_url_in_browser(current_video.video_url)
+ if current_video and current_video.video_url then
+ open_url_in_browser(current_video.video_url)
+ end
end
local function open_channel_in_browser()
- open_url_in_browser(current_video.channel_url)
+ if current_video and current_video.channel_url then
+ open_url_in_browser(current_video.channel_url)
+ end
end
local function _print_internal_playlist()
- local count = mp.get_property_number("playlist-count")
- print("Playlist contents:")
- for i = 0, count - 1 do
- local uri = mp.get_property(string.format("playlist/%d/filename", i))
- print(string.format("%d: %s", i, uri))
- end
+ local count = mp.get_property_number("playlist-count")
+ print("Playlist contents:")
+ for i = 0, count - 1 do
+ local uri = mp.get_property(string.format("playlist/%d/filename", i))
+ print(string.format("%d: %s", i, uri))
+ end
end
local function toggle_print()
- if destroyer ~= nil then
- destroyer()
- else
- YouTubeQueue.print_queue()
- end
+ if destroyer ~= nil then
+ destroyer()
+ else
+ YouTubeQueue.print_queue()
+ end
end
+
+-- Function to remove leading and trailing quotes from the first and last arguments of a command table in-place
+local function _remove_command_quotes(s)
+ -- if the first character of the first argument is a quote, remove it
+ if string.sub(s[1], 1, 1) == "'" or string.sub(s[1], 1, 1) == "\"" then
+ s[1] = string.sub(s[1], 2)
+ end
+ -- if the last character of the last argument is a quote, remove it
+ if string.sub(s[#s], -1) == "'" or string.sub(s[#s], -1) == "\"" then
+ s[#s] = string.sub(s[#s], 1, -2)
+ end
+end
+
+-- Function to split the clipboard_command into it's parts and return as a table
+local function _split_command(cmd)
+ local components = {}
+ for arg in cmd:gmatch("%S+") do
+ table.insert(components, arg)
+ end
+ _remove_command_quotes(components)
+ return components
+end
+
-- }}}
-- QUEUE GETTERS AND SETTERS {{{
function YouTubeQueue.get_video_at(idx)
- if idx <= 0 or idx > #video_queue then
- print_osd_message("Invalid video index", MSG_DURATION, style.error)
- return nil
- end
- return video_queue[idx]
+ if idx <= 0 or idx > #video_queue then
+ print_osd_message("Invalid video index", MSG_DURATION, style.error)
+ return nil
+ end
+ return video_queue[idx]
end
-- returns the content of the clipboard
function YouTubeQueue.get_clipboard_content()
- local command, args = options.clipboard_command:match("(%S+)%s+(%S+)")
- local res = mp.command_native({
- name = "subprocess",
- playback_only = false,
- capture_stdout = true,
- args = { command, args }
- })
+ local command = _split_command(options.clipboard_command)
+ for i, v in ipairs(command) do
+ print(i, v)
+ end
+ local res = mp.command_native({
+ name = "subprocess",
+ playback_only = false,
+ capture_stdout = true,
+ args = command
+ })
- if res.status ~= 0 then
- print_osd_message("Failed to get clipboard content", MSG_DURATION,
- style.error)
- return nil
- end
+ if res.status ~= 0 then
+ print_osd_message("Failed to get clipboard content", MSG_DURATION,
+ style.error)
+ return nil
+ end
- return res.stdout
+ return res.stdout
end
function YouTubeQueue.get_video_info(url)
- local res = mp.command_native({
- name = "subprocess",
- playback_only = false,
- capture_stdout = true,
- args = {
- "yt-dlp", "--print", "channel_url", "--print", "uploader",
- "--print", "title", "--playlist-items", "1", url
- }
- })
+ local res = mp.command_native({
+ name = "subprocess",
+ playback_only = false,
+ capture_stdout = true,
+ args = {
+ "yt-dlp", "--print", "channel_url", "--print", "uploader",
+ "--print", "title", "--playlist-items", "1", url
+ }
+ })
- if res.status ~= 0 then
- print_osd_message("Failed to get video info", MSG_DURATION, style.error)
- return nil
- end
+ if res.status ~= 0 then
+ print_osd_message("Failed to get video info", MSG_DURATION, style.error)
+ return nil
+ end
- local channel_url, uploader, title = res.stdout:match("(.*)\n(.*)\n(.*)\n")
- if channel_url == nil or uploader == nil or title == nil or channel_url ==
- "" or uploader == "" or title == "" then
- print_osd_message("Failed to get video info", MSG_DURATION, style.error)
- return nil
- end
+ local channel_url, uploader, title = res.stdout:match("(.*)\n(.*)\n(.*)\n")
+ if channel_url == nil or uploader == nil or title == nil or channel_url ==
+ "" or uploader == "" or title == "" then
+ print_osd_message("Failed to get video info", MSG_DURATION, style.error)
+ return nil
+ end
- return channel_url, uploader, title
+ return channel_url, uploader, title
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
+ destroy()
+ local current = current_video
+ if current and current.vidro_url and is_file(current.video_url) then
+ print_osd_message("Playing: " .. current.video_name, 3)
+ else
+ if current and current.video_url then
+ print_osd_message("Playing: " .. current.video_name .. ' by ' ..
+ current.channel_name, 3)
+ end
+ end
end
-- }}}
@@ -257,320 +289,324 @@ end
-- direction can be "NEXT" or "PREV". If nil, "next" is assumed
-- 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
- current_video = video_queue[index]
- return current_video
+ 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
+ current_video = video_queue[index]
+ return current_video
end
function YouTubeQueue.is_in_queue(url)
- for _, v in ipairs(video_queue) do
- if v.video_url == url then return true end
- end
- return false
+ for _, v in ipairs(video_queue) do
+ if v.video_url == url then return true end
+ end
+ return false
end
-- Function to find the index of the currently playing video
function YouTubeQueue.update_current_index()
- if #video_queue == 0 then return end
- local current_url = mp.get_property("path")
- for i, v in ipairs(video_queue) do
- if v.video_url == current_url then
- index = i
- selected_index = index
- current_video = YouTubeQueue.get_video_at(index)
- return
- end
- end
- -- if not found, reset the index
- index = 0
+ if #video_queue == 0 then return end
+ local current_url = mp.get_property("path")
+ for i, v in ipairs(video_queue) do
+ if v.video_url == current_url then
+ index = i
+ selected_index = index
+ current_video = YouTubeQueue.get_video_at(index)
+ return
+ end
+ end
+ -- if not found, reset the index
+ index = 0
end
function YouTubeQueue.mark_and_move_video()
- if marked_index == nil and selected_index ~= index then
- -- Mark the currently selected video for moving
- marked_index = selected_index
- else
- -- Move the previously marked video to the selected position
- YouTubeQueue.reorder_queue(marked_index, selected_index)
- -- print_osd_message("Video moved to the selected position.", 1.5)
- marked_index = nil -- Reset the marked index
- end
- -- Refresh the queue display
- YouTubeQueue.print_queue()
+ if marked_index == nil and selected_index ~= index then
+ -- Mark the currently selected video for moving
+ marked_index = selected_index
+ else
+ -- Move the previously marked video to the selected position
+ YouTubeQueue.reorder_queue(marked_index, selected_index)
+ -- print_osd_message("Video moved to the selected position.", 1.5)
+ marked_index = nil -- Reset the marked index
+ end
+ -- Refresh the queue display
+ YouTubeQueue.print_queue()
end
function YouTubeQueue.reorder_queue(from_index, to_index)
- if from_index == to_index or to_index == index then
- print_osd_message("No changes made.", 1.5)
- return
- end
- -- Check if the provided indices are within the bounds of the video_queue
- if from_index > 0 and from_index <= #video_queue and to_index > 0 and
- to_index <= #video_queue then
- -- move the video from the from_index to to_index in the internal playlist.
- -- playlist-move is 0-indexed
- if from_index < to_index and to_index == #video_queue then
- mp.commandv("playlist-move", from_index - 1, to_index)
- elseif from_index < to_index then
- mp.commandv("playlist-move", to_index - 1, from_index - 1)
- else
- mp.commandv("playlist-move", from_index - 1, to_index - 1)
- end
+ if from_index == to_index or to_index == index then
+ print_osd_message("No changes made.", 1.5)
+ return
+ end
+ -- Check if the provided indices are within the bounds of the video_queue
+ if from_index > 0 and from_index <= #video_queue and to_index > 0 and
+ to_index <= #video_queue then
+ -- move the video from the from_index to to_index in the internal playlist.
+ -- playlist-move is 0-indexed
+ if from_index < to_index and to_index == #video_queue then
+ mp.commandv("playlist-move", from_index - 1, to_index)
+ if to_index > index then index = index - 1 end
+ elseif from_index < to_index then
+ mp.commandv("playlist-move", from_index - 1, to_index)
+ if to_index > index then index = index - 1 end
+ else
+ mp.commandv("playlist-move", from_index - 1, to_index - 1)
+ end
- -- Remove from from_index and insert at to_index into YouTubeQueue
- local temp_video = video_queue[from_index]
- table.remove(video_queue, from_index)
- table.insert(video_queue, to_index, temp_video)
- else
- print_osd_message("Invalid indices for reordering. No changes made.",
- MSG_DURATION, style.error)
- end
+ -- Remove from from_index and insert at to_index into YouTubeQueue
+ local temp_video = video_queue[from_index]
+ table.remove(video_queue, from_index)
+ table.insert(video_queue, to_index, temp_video)
+ else
+ print_osd_message("Invalid indices for reordering. No changes made.",
+ MSG_DURATION, style.error)
+ end
end
function YouTubeQueue.print_queue(duration)
- timeout:kill()
- timeout:resume()
- local ass = assdraw.ass_new()
- local current_index = index
- if #video_queue > 0 then
- local half_limit = math.floor(options.display_limit / 2)
- local start_index, end_index
+ timeout:kill()
+ timeout:resume()
+ local ass = assdraw.ass_new()
+ local current_index = index
+ if #video_queue > 0 then
+ local half_limit = math.floor(options.display_limit / 2)
+ local start_index, end_index
- if selected_index <= half_limit then
- start_index = 1
- else
- start_index = selected_index - half_limit
- end
+ if selected_index <= half_limit then
+ start_index = 1
+ else
+ start_index = selected_index - half_limit
+ end
- end_index = start_index + options.display_limit - 1
- if end_index > #video_queue then end_index = #video_queue end
+ end_index = start_index + options.display_limit - 1
+ if end_index > #video_queue then end_index = #video_queue end
- ass:append(
- style.header .. "MPV-YOUTUBE-QUEUE{\\u0\\b0}" .. style.reset ..
- style.font .. "\n")
- local message
- for i = start_index, end_index do
- local prefix = (i == selected_index) and style.cursor ..
- options.cursor_icon .. "\\h" .. style.reset or
- "\\h\\h\\h"
- if i == current_index and i == selected_index then
- message = prefix .. style.hover_selected .. i .. ". " ..
- video_queue[i].video_name .. " - (" ..
- video_queue[i].channel_name .. ")" .. style.reset
- elseif i == current_index then
- message = prefix .. style.selected .. i .. ". " ..
- video_queue[i].video_name .. " - (" ..
- video_queue[i].channel_name .. ")" .. style.reset
- elseif i == selected_index then
- message = prefix .. style.hover .. i .. ". " ..
- video_queue[i].video_name .. " - (" ..
- video_queue[i].channel_name .. ")" .. style.reset
- else
- message = prefix .. style.reset .. i .. ". " ..
- video_queue[i].video_name .. " - (" ..
- video_queue[i].channel_name .. ")" .. style.reset
- end
- if i == marked_index then
- message =
- message .. " " .. style.marked .. options.marked_icon ..
- style.reset .. "\n"
- else
- message = message .. "\n"
- 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
- else
- print_osd_message("No videos in the queue or history.", duration,
- style.error)
- end
- destroyer = destroy
+ ass:append(
+ style.header .. "MPV-YOUTUBE-QUEUE{\\u0\\b0}" .. style.reset ..
+ style.font .. "\n")
+ local message
+ for i = start_index, end_index do
+ local prefix = (i == selected_index) and style.cursor ..
+ options.cursor_icon .. "\\h" .. style.reset or
+ "\\h\\h\\h"
+ if i == current_index and i == selected_index then
+ message = prefix .. style.hover_selected .. i .. ". " ..
+ video_queue[i].video_name .. " - (" ..
+ video_queue[i].channel_name .. ")" .. style.reset
+ elseif i == current_index then
+ message = prefix .. style.selected .. i .. ". " ..
+ video_queue[i].video_name .. " - (" ..
+ video_queue[i].channel_name .. ")" .. style.reset
+ elseif i == selected_index then
+ message = prefix .. style.hover .. i .. ". " ..
+ video_queue[i].video_name .. " - (" ..
+ video_queue[i].channel_name .. ")" .. style.reset
+ else
+ message = prefix .. style.reset .. i .. ". " ..
+ video_queue[i].video_name .. " - (" ..
+ video_queue[i].channel_name .. ")" .. style.reset
+ end
+ if i == marked_index then
+ message =
+ message .. " " .. style.marked .. options.marked_icon ..
+ style.reset .. "\n"
+ else
+ message = message .. "\n"
+ 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
+ else
+ print_osd_message("No videos in the queue or history.", duration,
+ style.error)
+ end
+ destroyer = destroy
end
function YouTubeQueue.move_cursor(amt)
- timeout:kill()
- timeout:resume()
- 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 amt == 1 and selected_index > 1 and selected_index < display_offset + 1 then
- display_offset = display_offset - math.abs(selected_index - amt)
- elseif amt == -1 and selected_index < #video_queue and selected_index >
- display_offset + options.display_limit then
- display_offset = display_offset + math.abs(selected_index - amt)
- end
- YouTubeQueue.print_queue()
+ timeout:kill()
+ timeout:resume()
+ 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 amt == 1 and selected_index > 1 and selected_index < display_offset + 1 then
+ display_offset = display_offset - math.abs(selected_index - amt)
+ elseif amt == -1 and selected_index < #video_queue and selected_index >
+ display_offset + options.display_limit then
+ display_offset = display_offset + math.abs(selected_index - amt)
+ end
+ YouTubeQueue.print_queue()
end
function YouTubeQueue.play_video_at(idx)
- if idx <= 0 or idx > #video_queue then
- print_osd_message("Invalid video index", MSG_DURATION, style.error)
- return nil
- end
- index = idx
- selected_index = idx
- mp.set_property_number("playlist-pos", index - 1) -- zero-based index
- YouTubeQueue.print_current_video()
- return current_video
+ if idx <= 0 or idx > #video_queue then
+ print_osd_message("Invalid video index", MSG_DURATION, style.error)
+ return nil
+ end
+ index = idx
+ selected_index = idx
+ mp.set_property_number("playlist-pos", index - 1) -- zero-based index
+ YouTubeQueue.print_current_video()
+ return current_video
end
-- play the next video in the queue
function YouTubeQueue.play_video(direction)
- direction = string.upper(direction)
- local video = YouTubeQueue.set_video(direction)
- if video == nil then
- print_osd_message("No video available.", MSG_DURATION, style.error)
- return
- end
- current_video = video
- selected_index = index
- -- 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
- if direction == "NEXT" and #video_queue > 1 then
- YouTubeQueue.play_video_at(index)
- elseif direction == "NEXT" and #video_queue == 1 then
- local state = mp.get_property("core-idle")
- -- yes if the video is loaded but not currently playing
- if state == "yes" then
- mp.commandv("loadfile", video.video_url, "replace")
- end
- elseif direction == "PREV" or direction == "PREVIOUS" then
- mp.set_property_number("playlist-pos", index - 1)
- end
- YouTubeQueue.print_current_video()
- sleep(MSG_DURATION)
+ direction = string.upper(direction)
+ local video = YouTubeQueue.set_video(direction)
+ if video == nil then
+ print_osd_message("No video available.", MSG_DURATION, style.error)
+ return
+ end
+ current_video = video
+ selected_index = index
+ -- 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
+ if direction == "NEXT" and #video_queue > 1 then
+ YouTubeQueue.play_video_at(index)
+ elseif direction == "NEXT" and #video_queue == 1 then
+ local state = mp.get_property("core-idle")
+ -- yes if the video is loaded but not currently playing
+ if state == "yes" then
+ mp.commandv("loadfile", video.video_url, "replace")
+ end
+ elseif direction == "PREV" or direction == "PREVIOUS" then
+ mp.set_property_number("playlist-pos", index - 1)
+ end
+ YouTubeQueue.print_current_video()
+ sleep(MSG_DURATION)
end
-- add the video to the queue from the clipboard or call from script-message
-- updates the internal playlist by default, pass 0 to disable
function YouTubeQueue.add_to_queue(url, update_internal_playlist)
- if update_internal_playlist == nil then update_internal_playlist = 0 end
- if url == nil or url == "" then
- url = YouTubeQueue.get_clipboard_content()
- if url == nil or url == "" then
- print_osd_message("Nothing found in the clipboard.", MSG_DURATION,
- style.error)
- return
- end
- end
- if YouTubeQueue.is_in_queue(url) then
- print_osd_message("Video already in queue.", MSG_DURATION, style.error)
- return
- end
+ if update_internal_playlist == nil then update_internal_playlist = 0 end
+ if url == nil or url == "" then
+ url = YouTubeQueue.get_clipboard_content()
+ if url == nil or url == "" then
+ print_osd_message("Nothing found in the clipboard.", MSG_DURATION,
+ style.error)
+ return
+ end
+ end
+ if YouTubeQueue.is_in_queue(url) then
+ print_osd_message("Video already in queue.", MSG_DURATION, style.error)
+ return
+ end
- local video, channel_url, channel_name, video_name
- if not is_file(url) then
- channel_url, channel_name, video_name = YouTubeQueue.get_video_info(url)
- url = remove_quotes(url)
- if (channel_url == nil or channel_name == nil or video_name == nil) or
- (channel_url == "" or channel_name == "" or video_name == "") then
- print_osd_message("Error getting video info.", MSG_DURATION,
- style.error)
- return
- else
- video = {
- video_url = url,
- video_name = video_name,
- channel_url = channel_url,
- channel_name = channel_name
- }
- end
- else
- channel_url, video_name = split_path(url)
- if channel_url == nil or video_name == nil or channel_url == "" or
- video_name == "" then
- print_osd_message("Error getting video info.", MSG_DURATION,
- style.error)
- return
- end
- video = {
- video_url = url,
- video_name = video_name,
- channel_url = channel_url,
- channel_name = "Local file"
- }
- end
+ local video, channel_url, channel_name, video_name
+ if not is_file(url) then
+ channel_url, channel_name, video_name = YouTubeQueue.get_video_info(url)
+ url = remove_quotes(url)
+ if (channel_url == nil or channel_name == nil or video_name == nil) or
+ (channel_url == "" or channel_name == "" or video_name == "") then
+ print_osd_message("Error getting video info.", MSG_DURATION,
+ style.error)
+ return
+ else
+ video = {
+ video_url = url,
+ video_name = video_name,
+ channel_url = channel_url,
+ channel_name = channel_name
+ }
+ end
+ else
+ channel_url, video_name = split_path(url)
+ if channel_url == nil or video_name == nil or channel_url == "" or
+ video_name == "" then
+ print_osd_message("Error getting video info.", MSG_DURATION,
+ style.error)
+ return
+ end
+ video = {
+ video_url = url,
+ video_name = video_name,
+ channel_url = channel_url,
+ channel_name = "Local file"
+ }
+ end
- table.insert(video_queue, video)
- -- if the queue was empty, start playing the video
- -- otherwise, add the video to the playlist
- if not current_video then
- YouTubeQueue.play_video("NEXT")
- elseif update_internal_playlist == 0 then
- mp.commandv("loadfile", url, "append-play")
- end
- print_osd_message("Added " .. video_name .. " to queue.", MSG_DURATION)
+ table.insert(video_queue, video)
+ -- if the queue was empty, start playing the video
+ -- otherwise, add the video to the playlist
+ if not current_video then
+ YouTubeQueue.play_video("NEXT")
+ elseif update_internal_playlist == 0 then
+ mp.commandv("loadfile", url, "append-play")
+ end
+ print_osd_message("Added " .. video_name .. " to queue.", MSG_DURATION)
end
function YouTubeQueue.download_video_at(idx)
- if idx < 0 or idx > #video_queue then return end
- local v = video_queue[idx]
- if is_file(v.video_url) then
- print_osd_message("Current video is a local file... doing nothing.",
- MSG_DURATION, style.error)
- return
- end
- local o = options
- local q = o.download_quality:sub(1, -2)
- local dl_dir = expanduser(o.download_directory)
+ if idx < 0 or idx > #video_queue then return end
+ local v = video_queue[idx]
+ if is_file(v.video_url) then
+ print_osd_message("Current video is a local file... doing nothing.",
+ MSG_DURATION, style.error)
+ return
+ end
+ local o = options
+ local q = o.download_quality:sub(1, -2)
+ local dl_dir = expanduser(o.download_directory)
- print_osd_message("Downloading " .. v.video_name .. "...", MSG_DURATION)
- -- Run the download command
- mp.command_native_async({
- name = "subprocess",
- capture_stderr = true,
- detach = true,
- args = {
- "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
- }
- }, function(success, _, err)
- if success then
- print_osd_message("Finished downloading " .. v.video_name .. ".",
- MSG_DURATION)
- else
- print_osd_message("Error downloading " .. v.video_name .. ": " ..
- err, MSG_DURATION, style.error)
- end
- end)
+ print_osd_message("Downloading " .. v.video_name .. "...", MSG_DURATION)
+ -- Run the download command
+ mp.command_native_async({
+ name = "subprocess",
+ capture_stderr = true,
+ detach = true,
+ args = {
+ "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
+ }
+ }, function(success, _, err)
+ if success then
+ print_osd_message("Finished downloading " .. v.video_name .. ".",
+ MSG_DURATION)
+ else
+ print_osd_message("Error downloading " .. v.video_name .. ": " ..
+ err, MSG_DURATION, style.error)
+ end
+ end)
end
function YouTubeQueue.remove_from_queue()
- if index == selected_index then
- print_osd_message("Cannot remove current video", MSG_DURATION,
- style.error)
- return
- end
- table.remove(video_queue, selected_index)
- mp.commandv("playlist-remove", selected_index - 1)
- print_osd_message("Deleted " .. current_video.video_name .. " from queue.",
- MSG_DURATION)
- if selected_index > 1 then selected_index = selected_index - 1 end
- index = index - 1
- YouTubeQueue.print_queue()
+ if index == selected_index then
+ print_osd_message("Cannot remove current video", MSG_DURATION,
+ style.error)
+ return
+ end
+ table.remove(video_queue, selected_index)
+ mp.commandv("playlist-remove", selected_index - 1)
+ if current_video and current_video.video_name then
+ print_osd_message("Deleted " .. current_video.video_name .. " from queue.",
+ MSG_DURATION)
+ end
+ if selected_index > 1 then selected_index = selected_index - 1 end
+ index = index - 1
+ YouTubeQueue.print_queue()
end
-- }}}
@@ -578,9 +614,9 @@ end
-- LISTENERS {{{
-- Function to be called when the end-file event is triggered
local function on_end_file(event)
- if event.reason == "eof" then -- The file ended normally
- YouTubeQueue.update_current_index()
- end
+ if event.reason == "eof" then -- The file ended normally
+ YouTubeQueue.update_current_index()
+ end
end
-- Function to be called when the track-changed event is triggered
@@ -588,47 +624,47 @@ local function on_track_changed() YouTubeQueue.update_current_index() end
-- Function to be called when the playback-restart event is triggered
local function on_playback_restart()
- local playlist_size = mp.get_property_number("playlist-count", 0)
- if current_video ~= nil and playlist_size > 1 then
- YouTubeQueue.update_current_index()
- elseif current_video == nil then
- local url = mp.get_property("path")
- YouTubeQueue.add_to_queue(url)
- end
+ local playlist_size = mp.get_property_number("playlist-count", 0)
+ if current_video ~= nil and playlist_size > 1 then
+ YouTubeQueue.update_current_index()
+ elseif current_video == nil then
+ local url = mp.get_property("path")
+ YouTubeQueue.add_to_queue(url)
+ end
end
-- }}}
-- KEY BINDINGS {{{
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",
- function() YouTubeQueue.play_video("NEXT") end)
+ function() YouTubeQueue.play_video("NEXT") end)
mp.add_key_binding(options.play_previous_in_queue, "play_prev_in_queue",
- function() YouTubeQueue.play_video("PREV") end)
+ function() YouTubeQueue.play_video("PREV") end)
mp.add_key_binding(options.print_queue, "print_queue", toggle_print)
mp.add_key_binding(options.move_cursor_up, "move_cursor_up",
- function() YouTubeQueue.move_cursor(1) end,
- { repeatable = true })
+ function() YouTubeQueue.move_cursor(1) end,
+ { repeatable = true })
mp.add_key_binding(options.move_cursor_down, "move_cursor_down",
- function() YouTubeQueue.move_cursor(-1) end,
- { repeatable = true })
+ function() YouTubeQueue.move_cursor(-1) end,
+ { repeatable = true })
mp.add_key_binding(options.play_selected_video, "play_selected_video",
- function() YouTubeQueue.play_video_at(selected_index) end)
+ function() YouTubeQueue.play_video_at(selected_index) end)
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",
- YouTubeQueue.print_current_video)
+ YouTubeQueue.print_current_video)
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",
- function() YouTubeQueue.download_video_at(index) end)
+ function() YouTubeQueue.download_video_at(index) end)
mp.add_key_binding(options.download_selected_video, "download_selected_video",
- function() YouTubeQueue.download_video_at(selected_index) end)
+ function() YouTubeQueue.download_video_at(selected_index) end)
mp.add_key_binding(options.move_video, "move_video",
- YouTubeQueue.mark_and_move_video)
+ YouTubeQueue.mark_and_move_video)
mp.add_key_binding(options.remove_from_queue, "delete_video",
- YouTubeQueue.remove_from_queue)
+ YouTubeQueue.remove_from_queue)
mp.register_event("end-file", on_end_file)
mp.register_event("track-changed", on_track_changed)