mirror of
https://github.com/ksyasuda/mpv-youtube-queue.git
synced 2024-10-28 04:44:11 -07:00
Merge 827eb80d6e
into 0739b93e3a
This commit is contained in:
commit
8061c15b69
15
README.md
15
README.md
@ -63,21 +63,20 @@ This script requires the following software to be installed on the system
|
|||||||
- `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 - 10`: 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
|
||||||
- `downloader - curl`: The name of the program to use to download the video
|
- `downloader - curl`: The name of the program to use to download the video
|
||||||
- `font_name - JetBrains Mono`: The name of the font to use
|
- `font_name - JetBrains Mono`: The name of the font to use
|
||||||
- `font_size - 12`: Size of the font
|
- `font_size - 12`: Size of the font
|
||||||
- `marked_icon - ⇅`: The icon to use to mark a video as ready to be moved in
|
- `marked_icon - ⇅`: The icon to use to mark a video as ready to be moved in the queue
|
||||||
the queue
|
|
||||||
- `menu_timeout - 5`: The number of seconds until the menu times out
|
- `menu_timeout - 5`: The number of seconds until the menu times out
|
||||||
- `show_errors - yes`: Show error messages on the OSD
|
- `show_errors - yes`: Show error messages on the OSD
|
||||||
- `ytdlp_file_format - mp4`: The preferred file format for downloaded videos
|
- `ytdlp_file_format - mp4`: The preferred file format for downloaded videos
|
||||||
- `ytdlp_output_template - %(uploader)s/%(title)s.%(ext)s`: The [yt-dlp output
|
- `ytdlp_output_template - %(uploader)s/%(title)s.%(ext)s`: The [yt-dlp output template string](https://github.com/yt-dlp/yt-dlp#output-template)
|
||||||
template string](https://github.com/yt-dlp/yt-dlp#output-template)
|
- Full path with the default `download_directory` is: `~/videos/YouTube/<uploader>/<title>.<ext>`
|
||||||
- Full path with the default `download_directory`
|
- `use_history_db - no`: Enable watch history tracking through integration with [mpv-youtube-queue-server](https://gitea.suda.codes/sudacode/mpv-youtube-queue-server)
|
||||||
is: `~/videos/YouTube/<uploader>/<title>.<ext>`
|
- `backend_host`: ip or hostname of the backend server
|
||||||
|
- `backend_port`: port to connect to for the backend server
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -26,3 +26,6 @@ menu_timeout=5
|
|||||||
show_errors=yes
|
show_errors=yes
|
||||||
ytdlp_file_format=mp4
|
ytdlp_file_format=mp4
|
||||||
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
|
ytdlp_output_template=%(uploader)s/%(title)s.%(ext)s
|
||||||
|
use_history_db=no
|
||||||
|
backend_host=http://localhost
|
||||||
|
backend_port=42069
|
||||||
|
@ -29,6 +29,7 @@ local marked_index = nil
|
|||||||
local current_video = nil
|
local current_video = nil
|
||||||
local destroyer = nil
|
local destroyer = nil
|
||||||
local timeout
|
local timeout
|
||||||
|
local debug = true
|
||||||
|
|
||||||
local options = {
|
local options = {
|
||||||
add_to_queue = "ctrl+a",
|
add_to_queue = "ctrl+a",
|
||||||
@ -58,7 +59,12 @@ local options = {
|
|||||||
menu_timeout = 5,
|
menu_timeout = 5,
|
||||||
show_errors = true,
|
show_errors = true,
|
||||||
ytdlp_file_format = "mp4",
|
ytdlp_file_format = "mp4",
|
||||||
ytdlp_output_template = "%(uploader)s/%(title)s.%(ext)s"
|
ytdlp_output_template = "%(uploader)s/%(title)s.%(ext)s",
|
||||||
|
use_history_db = false,
|
||||||
|
backend_host = "http://localhost",
|
||||||
|
backend_port = "42069",
|
||||||
|
save_queue = "ctrl+s",
|
||||||
|
load_queue = "ctrl+l"
|
||||||
}
|
}
|
||||||
mp.options.read_options(options, "mpv-youtube-queue")
|
mp.options.read_options(options, "mpv-youtube-queue")
|
||||||
|
|
||||||
@ -102,19 +108,17 @@ local style = {
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- HELPERS {{{
|
-- HELPERS {{{
|
||||||
|
|
||||||
-- surround string with single quotes if it does not already have them
|
-- surround string with single quotes if it does not already have them
|
||||||
local function surround_with_quotes(s)
|
local function surround_with_quotes(s)
|
||||||
if string.sub(s, 0, 1) == "'" and string.sub(s, -1) == "'" then
|
if string.sub(s, 0, 1) == '"' and string.sub(s, -1) == '"' then
|
||||||
return s
|
return
|
||||||
else
|
else
|
||||||
return "'" .. s .. "'"
|
return '"' .. s .. '"'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function remove_quotes(s) return string.gsub(s, "'", "") end
|
local function strip(s) return string.gsub(s, "['\n\r]", "") end
|
||||||
|
|
||||||
-- run sleep shell command for n seconds
|
|
||||||
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
|
||||||
@ -128,8 +132,11 @@ 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 return false end
|
if debug and type(result) == "table" then
|
||||||
return result.is_file
|
print("IS_FILE() check: " .. tostring(result.is_file))
|
||||||
|
end
|
||||||
|
if result == nil or type(result) ~= "table" then return false end
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- returns the filename given a path (e.g. /home/user/file.txt -> file.txt)
|
-- returns the filename given a path (e.g. /home/user/file.txt -> file.txt)
|
||||||
@ -206,6 +213,113 @@ local function _split_command(cmd)
|
|||||||
return components
|
return components
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function YouTubeQueue._add_to_history_db(v)
|
||||||
|
if not options.use_history_db then return false end
|
||||||
|
local url = options.backend_host .. ":" .. options.backend_port ..
|
||||||
|
"/add_video"
|
||||||
|
local command = {
|
||||||
|
"curl", "-X", "POST", url, "-H", "Content-Type: application/json", "-d",
|
||||||
|
string.format(
|
||||||
|
'{"video_url": "%s", "video_name": "%s", "channel_url": "%s", "channel_name": "%s"}',
|
||||||
|
v.video_url, v.video_name, v.channel_url, v.channel_name)
|
||||||
|
}
|
||||||
|
mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = command
|
||||||
|
}, function(success, _, err)
|
||||||
|
if not success then
|
||||||
|
print_osd_message("Failed to send video data to backend: " .. err,
|
||||||
|
MSG_DURATION, style.error)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Returns a list of URLs in the queue from index + 1 to the end
|
||||||
|
function YouTubeQueue._get_urls(start_index)
|
||||||
|
if start_index < 0 or start_index + 1 >= #video_queue then return nil end
|
||||||
|
local urls = {}
|
||||||
|
for i = start_index + 1, #video_queue do
|
||||||
|
table.insert(urls, video_queue[i].video_url)
|
||||||
|
end
|
||||||
|
return urls
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Converts to json
|
||||||
|
function YouTubeQueue._convert_to_json(key, val)
|
||||||
|
if val == nil then return end
|
||||||
|
if type(val) ~= "table" then return "{" .. key .. ":" .. val .. "}" end
|
||||||
|
local json = string.format('{"%s": [', key)
|
||||||
|
for i, v in ipairs(val) do
|
||||||
|
json = json .. '"' .. v .. '"'
|
||||||
|
if i < #val then json = json .. ", " end
|
||||||
|
end
|
||||||
|
json = json .. "]}"
|
||||||
|
return json
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Saves the remainder of the videos in the queue (all videos after the currently playing
|
||||||
|
-- video) to the history database
|
||||||
|
function YouTubeQueue.save_queue()
|
||||||
|
if not options.use_history_db then return false end
|
||||||
|
local url = options.backend_host .. ":" .. options.backend_port ..
|
||||||
|
"/save_queue"
|
||||||
|
local data = YouTubeQueue._convert_to_json("urls",
|
||||||
|
YouTubeQueue._get_urls(index))
|
||||||
|
if data == nil then
|
||||||
|
print_osd_message("Failed to save queue: No videos remaining in queue",
|
||||||
|
MSG_DURATION, style.error)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if debug then print("Data: " .. data) end
|
||||||
|
local command = {
|
||||||
|
"curl", "-X", "POST", url, "-H", "Content-Type: application/json", "-d",
|
||||||
|
data
|
||||||
|
}
|
||||||
|
if debug then
|
||||||
|
print("Saving queue to history")
|
||||||
|
print("Command: " .. table.concat(command, " "))
|
||||||
|
end
|
||||||
|
mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = command
|
||||||
|
}, function(success, _, err)
|
||||||
|
if not success then
|
||||||
|
print_osd_message("Failed to save queue: " .. err, MSG_DURATION,
|
||||||
|
style.error)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- loads the queue from the backend
|
||||||
|
function YouTubeQueue.load_queue()
|
||||||
|
if not options.use_history_db then return false end
|
||||||
|
local url = options.backend_host .. ":" .. options.backend_port ..
|
||||||
|
"/load_queue"
|
||||||
|
local command = { "curl", "-X", "GET", url }
|
||||||
|
|
||||||
|
mp.command_native_async({
|
||||||
|
name = "subprocess",
|
||||||
|
playback_only = false,
|
||||||
|
capture_stdout = true,
|
||||||
|
args = command
|
||||||
|
}, function(success, result, err)
|
||||||
|
if not success then
|
||||||
|
print_osd_message("Failed to load queue: " .. err, MSG_DURATION,
|
||||||
|
style.error)
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
for i in result do YouTubeQueue.add_to_queue(i) end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- QUEUE GETTERS AND SETTERS {{{
|
-- QUEUE GETTERS AND SETTERS {{{
|
||||||
@ -266,8 +380,8 @@ end
|
|||||||
function YouTubeQueue.print_current_video()
|
function YouTubeQueue.print_current_video()
|
||||||
destroy()
|
destroy()
|
||||||
local current = current_video
|
local current = current_video
|
||||||
if current and current.vidro_url and is_file(current.video_url) then
|
if current and current.vidro_url ~= "" and is_file(current.video_url) then
|
||||||
print_osd_message("Playing: " .. current.video_name, 3)
|
print_osd_message("Playing: " .. current.video_url, 3)
|
||||||
else
|
else
|
||||||
if current and current.video_url then
|
if current and current.video_url then
|
||||||
print_osd_message("Playing: " .. current.video_name .. ' by ' ..
|
print_osd_message("Playing: " .. current.video_name .. ' by ' ..
|
||||||
@ -310,14 +424,19 @@ function YouTubeQueue.is_in_queue(url)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Function to find the index of the currently playing video
|
-- Function to find the index of the currently playing video
|
||||||
function YouTubeQueue.update_current_index()
|
function YouTubeQueue.update_current_index(update_history)
|
||||||
|
if debug then print("Updating current index") end
|
||||||
if #video_queue == 0 then return end
|
if #video_queue == 0 then return end
|
||||||
|
if update_history == nil then update_history = false end
|
||||||
local current_url = mp.get_property("path")
|
local current_url = mp.get_property("path")
|
||||||
for i, v in ipairs(video_queue) do
|
for i, v in ipairs(video_queue) do
|
||||||
if v.video_url == current_url then
|
if v.video_url == current_url then
|
||||||
index = i
|
index = i
|
||||||
selected_index = index
|
selected_index = index
|
||||||
current_video = YouTubeQueue.get_video_at(index)
|
current_video = YouTubeQueue.get_video_at(index)
|
||||||
|
if update_history then
|
||||||
|
YouTubeQueue._add_to_history_db(current_video)
|
||||||
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -457,6 +576,7 @@ function YouTubeQueue.play_video_at(idx)
|
|||||||
end
|
end
|
||||||
index = idx
|
index = idx
|
||||||
selected_index = idx
|
selected_index = idx
|
||||||
|
current_video = video_queue[index]
|
||||||
mp.set_property_number("playlist-pos", index - 1) -- zero-based index
|
mp.set_property_number("playlist-pos", index - 1) -- zero-based index
|
||||||
YouTubeQueue.print_current_video()
|
YouTubeQueue.print_current_video()
|
||||||
return current_video
|
return current_video
|
||||||
@ -486,7 +606,6 @@ function YouTubeQueue.play_video(direction)
|
|||||||
mp.set_property_number("playlist-pos", index - 1)
|
mp.set_property_number("playlist-pos", index - 1)
|
||||||
end
|
end
|
||||||
YouTubeQueue.print_current_video()
|
YouTubeQueue.print_current_video()
|
||||||
sleep(MSG_DURATION)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- add the video to the queue from the clipboard or call from script-message
|
-- add the video to the queue from the clipboard or call from script-message
|
||||||
@ -507,9 +626,9 @@ function YouTubeQueue.add_to_queue(url, update_internal_playlist)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local video, channel_url, channel_name, video_name
|
local video, channel_url, channel_name, video_name
|
||||||
|
url = strip(url)
|
||||||
if not is_file(url) then
|
if not is_file(url) then
|
||||||
channel_url, channel_name, video_name = YouTubeQueue.get_video_info(url)
|
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
|
if (channel_url == nil or channel_name == nil or video_name == nil) or
|
||||||
(channel_url == "" or channel_name == "" or video_name == "") then
|
(channel_url == "" or channel_name == "" or video_name == "") then
|
||||||
print_osd_message("Error getting video info.", MSG_DURATION,
|
print_osd_message("Error getting video info.", MSG_DURATION,
|
||||||
@ -608,23 +727,36 @@ end
|
|||||||
|
|
||||||
-- LISTENERS {{{
|
-- LISTENERS {{{
|
||||||
-- Function to be called when the end-file event is triggered
|
-- Function to be called when the end-file event is triggered
|
||||||
|
-- This function is called when the current file ends or when moving to the
|
||||||
|
-- next or previous item in the internal playlist
|
||||||
local function on_end_file(event)
|
local function on_end_file(event)
|
||||||
|
if debug then print("End file event triggered: " .. event.reason) end
|
||||||
if event.reason == "eof" then -- The file ended normally
|
if event.reason == "eof" then -- The file ended normally
|
||||||
YouTubeQueue.update_current_index()
|
YouTubeQueue.update_current_index(true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Function to be called when the track-changed event is triggered
|
-- Function to be called when the track-changed event is triggered
|
||||||
local function on_track_changed() YouTubeQueue.update_current_index() end
|
local function on_track_changed()
|
||||||
|
if debug then print("Track changed event triggered.") end
|
||||||
|
YouTubeQueue.update_current_index()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function on_file_loaded()
|
||||||
|
if debug then print("Load file event triggered.") end
|
||||||
|
YouTubeQueue.update_current_index(true)
|
||||||
|
end
|
||||||
|
|
||||||
-- Function to be called when the playback-restart event is triggered
|
-- Function to be called when the playback-restart event is triggered
|
||||||
local function on_playback_restart()
|
local function on_playback_restart()
|
||||||
|
if debug then print("Playback restart event triggered.") end
|
||||||
local playlist_size = mp.get_property_number("playlist-count", 0)
|
local playlist_size = mp.get_property_number("playlist-count", 0)
|
||||||
if current_video ~= nil and playlist_size > 1 then
|
-- if current_video ~= nil and playlist_size > 1 then
|
||||||
YouTubeQueue.update_current_index()
|
-- YouTubeQueue.update_current_index()
|
||||||
elseif current_video == nil then
|
if current_video == nil then
|
||||||
local url = mp.get_property("path")
|
local url = mp.get_property("path")
|
||||||
YouTubeQueue.add_to_queue(url)
|
YouTubeQueue.add_to_queue(url)
|
||||||
|
YouTubeQueue._add_to_history_db(current_video)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -660,10 +792,13 @@ 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",
|
mp.add_key_binding(options.remove_from_queue, "delete_video",
|
||||||
YouTubeQueue.remove_from_queue)
|
YouTubeQueue.remove_from_queue)
|
||||||
|
mp.add_key_binding(options.save_queue, "save_queue", YouTubeQueue.save_queue)
|
||||||
|
mp.add_key_binding(options.load_queue, "load_queue", YouTubeQueue.load_queue)
|
||||||
|
|
||||||
mp.register_event("end-file", on_end_file)
|
mp.register_event("end-file", on_end_file)
|
||||||
mp.register_event("track-changed", on_track_changed)
|
mp.register_event("track-changed", on_track_changed)
|
||||||
mp.register_event("playback-restart", on_playback_restart)
|
mp.register_event("playback-restart", on_playback_restart)
|
||||||
|
mp.register_event("file-loaded", on_file_loaded)
|
||||||
|
|
||||||
-- keep for backwards compatibility
|
-- keep for backwards compatibility
|
||||||
mp.register_script_message("add_to_queue", YouTubeQueue.add_to_queue)
|
mp.register_script_message("add_to_queue", YouTubeQueue.add_to_queue)
|
||||||
|
Loading…
Reference in New Issue
Block a user