mirror of
https://github.com/ksyasuda/mpv-youtube-queue.git
synced 2026-03-22 18:11:27 -07:00
Add LRU caching for video info and title truncation
- Implemented an LRU cache for video information with a maximum size of 100 entries to improve performance. - Added a function to truncate video titles to a specified maximum length for better display. - Updated the `get_video_info` function to utilize the cache, reducing redundant data fetching. - Enhanced the `build_osd_row` function to use truncated titles. - Introduced a new function to sync the video queue with mpv's internal playlist on load and playback restart events.
This commit is contained in:
@@ -31,6 +31,11 @@ local destroyer = nil
|
|||||||
local timeout
|
local timeout
|
||||||
local debug = false
|
local debug = false
|
||||||
|
|
||||||
|
-- LRU cache for video info with bounded size
|
||||||
|
local VIDEO_INFO_CACHE_MAX_SIZE = 100
|
||||||
|
local video_info_cache = {}
|
||||||
|
local video_info_cache_order = {} -- Tracks access order for LRU eviction
|
||||||
|
|
||||||
local options = {
|
local options = {
|
||||||
add_to_queue = "ctrl+a",
|
add_to_queue = "ctrl+a",
|
||||||
download_current_video = "ctrl+d",
|
download_current_video = "ctrl+d",
|
||||||
@@ -67,6 +72,8 @@ local options = {
|
|||||||
save_queue_alt = "ctrl+S",
|
save_queue_alt = "ctrl+S",
|
||||||
default_save_method = "unwatched",
|
default_save_method = "unwatched",
|
||||||
load_queue = "ctrl+l",
|
load_queue = "ctrl+l",
|
||||||
|
-- Title truncation
|
||||||
|
max_title_length = 60,
|
||||||
}
|
}
|
||||||
mp.options.read_options(options, "mpv-youtube-queue")
|
mp.options.read_options(options, "mpv-youtube-queue")
|
||||||
|
|
||||||
@@ -79,15 +86,16 @@ end
|
|||||||
timeout = mp.add_periodic_timer(options.menu_timeout, destroy)
|
timeout = mp.add_periodic_timer(options.menu_timeout, destroy)
|
||||||
|
|
||||||
-- STYLE {{{
|
-- STYLE {{{
|
||||||
|
-- Catppuccin Macchiato color palette (BGR format for ASS)
|
||||||
local colors = {
|
local colors = {
|
||||||
error = "676EFF",
|
error = "9687ED", -- Red (#ed8796)
|
||||||
selected = "F993BD",
|
selected = "F5BDE6", -- Pink (#f5bde6)
|
||||||
hover_selected = "FAA9CA",
|
hover_selected = "C6C6F0", -- Flamingo (#f0c6c6)
|
||||||
cursor = "FDE98B",
|
cursor = "9FD4EE", -- Yellow (#eed49f)
|
||||||
header = "8CFAF1",
|
header = "CAD58B", -- Teal (#8bd5ca)
|
||||||
hover = "F2F8F8",
|
hover = "F8BDB7", -- Lavender (#b7bdf8)
|
||||||
text = "BFBFBF",
|
text = "E0C0B8", -- Subtext1 (#b8c0e0)
|
||||||
marked = "C679FF",
|
marked = "F6A0C6", -- Mauve (#c6a0f6)
|
||||||
}
|
}
|
||||||
|
|
||||||
local notransparent = "\\alpha&H00&"
|
local notransparent = "\\alpha&H00&"
|
||||||
@@ -117,6 +125,69 @@ local style = {
|
|||||||
|
|
||||||
-- HELPERS {{{
|
-- HELPERS {{{
|
||||||
|
|
||||||
|
--- Adds an item to the LRU cache, evicting the oldest entry if cache is full
|
||||||
|
--- @param url string - the URL key
|
||||||
|
--- @param info table - the video info to cache
|
||||||
|
local function cache_video_info(url, info)
|
||||||
|
-- If already in cache, remove from order list (will be re-added at end)
|
||||||
|
if video_info_cache[url] then
|
||||||
|
for i, cached_url in ipairs(video_info_cache_order) do
|
||||||
|
if cached_url == url then
|
||||||
|
table.remove(video_info_cache_order, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Evict oldest entry if cache is full
|
||||||
|
while #video_info_cache_order >= VIDEO_INFO_CACHE_MAX_SIZE do
|
||||||
|
local oldest_url = table.remove(video_info_cache_order, 1)
|
||||||
|
video_info_cache[oldest_url] = nil
|
||||||
|
if debug then
|
||||||
|
print("LRU cache evicted: " .. oldest_url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Add to cache and order list
|
||||||
|
video_info_cache[url] = info
|
||||||
|
table.insert(video_info_cache_order, url)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets an item from the LRU cache, updating access order
|
||||||
|
--- @param url string - the URL key
|
||||||
|
--- @return table | nil - the cached video info, or nil if not found
|
||||||
|
local function get_cached_video_info(url)
|
||||||
|
local info = video_info_cache[url]
|
||||||
|
if info then
|
||||||
|
-- Move to end of order list (most recently used)
|
||||||
|
for i, cached_url in ipairs(video_info_cache_order) do
|
||||||
|
if cached_url == url then
|
||||||
|
table.remove(video_info_cache_order, i)
|
||||||
|
table.insert(video_info_cache_order, url)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return info
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Truncates a string to a maximum length, adding ellipsis if truncated
|
||||||
|
--- @param s string - the string to truncate
|
||||||
|
--- @param max_len number - the maximum length
|
||||||
|
--- @return string - the truncated string
|
||||||
|
local function truncate_string(s, max_len)
|
||||||
|
if not s or max_len <= 0 then
|
||||||
|
return s or ""
|
||||||
|
end
|
||||||
|
if #s <= max_len then
|
||||||
|
return s
|
||||||
|
end
|
||||||
|
if max_len <= 3 then
|
||||||
|
return string.sub(s, 1, max_len)
|
||||||
|
end
|
||||||
|
return string.sub(s, 1, max_len - 3) .. "..."
|
||||||
|
end
|
||||||
|
|
||||||
--- surround string with single quotes if it does not already have them
|
--- surround string with single quotes if it does not already have them
|
||||||
--- @param s string - the string to surround with quotes
|
--- @param s string - the string to surround with quotes
|
||||||
--- @return string | nil - the string surrounded with quotes
|
--- @return string | nil - the string surrounded with quotes
|
||||||
@@ -251,7 +322,8 @@ end
|
|||||||
--- @param channel_name string - the name of the channel
|
--- @param channel_name string - the name of the channel
|
||||||
--- @return string - the OSD row
|
--- @return string - the OSD row
|
||||||
local function build_osd_row(prefix, s, i, video_name, channel_name)
|
local function build_osd_row(prefix, s, i, video_name, channel_name)
|
||||||
return prefix .. s .. i .. ". " .. video_name .. " - (" .. channel_name .. ")"
|
local truncated_name = truncate_string(video_name, options.max_title_length)
|
||||||
|
return prefix .. s .. i .. ". " .. truncated_name .. " - (" .. channel_name .. ")"
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Helper function to determine display range for queue items
|
--- Helper function to determine display range for queue items
|
||||||
@@ -338,7 +410,20 @@ local function convert_to_json(key, val)
|
|||||||
json = json .. "}"
|
json = json .. "}"
|
||||||
return json
|
return json
|
||||||
else
|
else
|
||||||
if type(val) == "string" then
|
-- Handle array values (table as val)
|
||||||
|
if type(val) == "table" then
|
||||||
|
local arr = "["
|
||||||
|
local first = true
|
||||||
|
for _, v in ipairs(val) do
|
||||||
|
if not first then
|
||||||
|
arr = arr .. ", "
|
||||||
|
end
|
||||||
|
first = false
|
||||||
|
arr = arr .. string.format('"%s"', v)
|
||||||
|
end
|
||||||
|
arr = arr .. "]"
|
||||||
|
return string.format('{"%s": %s}', key, arr)
|
||||||
|
elseif type(val) == "string" then
|
||||||
return string.format('{"%s": "%s"}', key, val)
|
return string.format('{"%s": "%s"}', key, val)
|
||||||
else
|
else
|
||||||
return string.format('{"%s": %s}', key, tostring(val))
|
return string.format('{"%s": %s}', key, tostring(val))
|
||||||
@@ -392,6 +477,19 @@ end
|
|||||||
--- @param url string - the URL to get the video info from
|
--- @param url string - the URL to get the video info from
|
||||||
--- @return table | nil - a table containing the video information
|
--- @return table | nil - a table containing the video information
|
||||||
function YouTubeQueue.get_video_info(url)
|
function YouTubeQueue.get_video_info(url)
|
||||||
|
-- Check LRU cache first
|
||||||
|
local cached = get_cached_video_info(url)
|
||||||
|
if cached then
|
||||||
|
if debug then
|
||||||
|
print("Cache hit for URL: " .. url)
|
||||||
|
end
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
if debug then
|
||||||
|
print("Cache miss for URL: " .. url)
|
||||||
|
end
|
||||||
|
|
||||||
print_osd_message("Getting video info...", MSG_DURATION * 2)
|
print_osd_message("Getting video info...", MSG_DURATION * 2)
|
||||||
local res = mp.command_native({
|
local res = mp.command_native({
|
||||||
name = "subprocess",
|
name = "subprocess",
|
||||||
@@ -443,6 +541,8 @@ function YouTubeQueue.get_video_info(url)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Cache the result with LRU eviction
|
||||||
|
cache_video_info(url, info)
|
||||||
return info
|
return info
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -450,7 +550,7 @@ 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.video_url ~= "" and is_file(current.video_url) then
|
||||||
print_osd_message("Playing: " .. current.video_url, 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
|
||||||
@@ -557,26 +657,36 @@ function YouTubeQueue.reorder_queue(from_index, to_index)
|
|||||||
end
|
end
|
||||||
-- Check if the provided indices are within the bounds of the video_queue
|
-- 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
|
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.
|
-- mpv's playlist-move moves entry at index1 to position before index2 (0-indexed)
|
||||||
-- playlist-move is 0-indexed
|
-- When moving to end of playlist, use playlist count as target
|
||||||
if from_index < to_index and to_index == #video_queue then
|
local mpv_from = from_index - 1
|
||||||
mp.commandv("playlist-move", from_index - 1, to_index)
|
local mpv_to
|
||||||
if to_index > index then
|
if from_index < to_index then
|
||||||
index = index - 1
|
-- Moving forward: playlist-move needs the position after target
|
||||||
end
|
mpv_to = to_index
|
||||||
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
|
else
|
||||||
mp.commandv("playlist-move", from_index - 1, to_index - 1)
|
-- Moving backward: playlist-move needs the target position
|
||||||
|
mpv_to = to_index - 1
|
||||||
end
|
end
|
||||||
|
mp.commandv("playlist-move", mpv_from, mpv_to)
|
||||||
|
|
||||||
-- Remove from from_index and insert at to_index into YouTubeQueue
|
-- Update our queue: remove from old position, insert at new
|
||||||
local temp_video = video_queue[from_index]
|
local temp_video = video_queue[from_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)
|
||||||
|
|
||||||
|
-- Update current index if affected by the move
|
||||||
|
if from_index == index then
|
||||||
|
-- We moved the currently playing video
|
||||||
|
index = to_index
|
||||||
|
elseif from_index < index and to_index >= index then
|
||||||
|
-- Moved an item from before current to at/after current: current shifts back
|
||||||
|
index = index - 1
|
||||||
|
elseif from_index > index and to_index <= index then
|
||||||
|
-- Moved an item from after current to at/before current: current shifts forward
|
||||||
|
index = index + 1
|
||||||
|
end
|
||||||
|
selected_index = to_index
|
||||||
else
|
else
|
||||||
print_osd_message("Invalid indices for reordering. No changes made.", MSG_DURATION, style.error)
|
print_osd_message("Invalid indices for reordering. No changes made.", MSG_DURATION, style.error)
|
||||||
end
|
end
|
||||||
@@ -597,7 +707,13 @@ function YouTubeQueue.print_queue(duration)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local ass = assdraw.ass_new()
|
local ass = assdraw.ass_new()
|
||||||
ass:append(style.header .. "MPV-YOUTUBE-QUEUE{\\u0\\b0}" .. style.reset .. style.font .. "\n")
|
local position_indicator = ""
|
||||||
|
if index > 0 then
|
||||||
|
position_indicator = " [" .. index .. "/" .. #video_queue .. "]"
|
||||||
|
else
|
||||||
|
position_indicator = " [" .. #video_queue .. " videos]"
|
||||||
|
end
|
||||||
|
ass:append(style.header .. "MPV-YOUTUBE-QUEUE" .. position_indicator .. "{\\u0\\b0}" .. style.reset .. style.font .. "\n")
|
||||||
|
|
||||||
local start_index, end_index = get_display_range(#video_queue, selected_index, options.display_limit)
|
local start_index, end_index = get_display_range(#video_queue, selected_index, options.display_limit)
|
||||||
|
|
||||||
@@ -750,7 +866,7 @@ end
|
|||||||
--- @param idx number - the index of the video to download
|
--- @param idx number - the index of the video to download
|
||||||
--- @return boolean - true if the video was downloaded successfully, false otherwise
|
--- @return boolean - true if the video was downloaded successfully, false otherwise
|
||||||
function YouTubeQueue.download_video_at(idx)
|
function YouTubeQueue.download_video_at(idx)
|
||||||
if idx < 0 or idx > #video_queue then
|
if idx <= 0 or idx > #video_queue then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local v = video_queue[idx]
|
local v = video_queue[idx]
|
||||||
@@ -806,10 +922,11 @@ function YouTubeQueue.remove_from_queue()
|
|||||||
print_osd_message("Cannot remove current video", MSG_DURATION, style.error)
|
print_osd_message("Cannot remove current video", MSG_DURATION, style.error)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
local removed_video = video_queue[selected_index]
|
||||||
table.remove(video_queue, selected_index)
|
table.remove(video_queue, selected_index)
|
||||||
mp.commandv("playlist-remove", selected_index - 1)
|
mp.commandv("playlist-remove", selected_index - 1)
|
||||||
if current_video and current_video.video_name then
|
if removed_video and removed_video.video_name then
|
||||||
print_osd_message("Deleted " .. current_video.video_name .. " from queue.", MSG_DURATION)
|
print_osd_message("Deleted " .. removed_video.video_name .. " from queue.", MSG_DURATION)
|
||||||
end
|
end
|
||||||
if selected_index > 1 then
|
if selected_index > 1 then
|
||||||
selected_index = selected_index - 1
|
selected_index = selected_index - 1
|
||||||
@@ -940,8 +1057,11 @@ function YouTubeQueue.load_queue()
|
|||||||
local l = result.stdout:sub(2, -3)
|
local l = result.stdout:sub(2, -3)
|
||||||
local item
|
local item
|
||||||
for turl in l:gmatch("[^,]+") do
|
for turl in l:gmatch("[^,]+") do
|
||||||
item = turl:match("^%s*(.-)%s*$"):gsub('"', "'")
|
local trimmed = turl:match("^%s*(.-)%s*$")
|
||||||
table.insert(urls, item)
|
if trimmed then
|
||||||
|
item = trimmed:gsub('"', "'")
|
||||||
|
table.insert(urls, item)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
for _, turl in ipairs(urls) do
|
for _, turl in ipairs(urls) do
|
||||||
YouTubeQueue.add_to_queue(turl, 0)
|
YouTubeQueue.add_to_queue(turl, 0)
|
||||||
@@ -952,6 +1072,55 @@ function YouTubeQueue.load_queue()
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Function to sync the video queue with mpv's internal playlist
|
||||||
|
--- @return boolean - true if sync was successful, false otherwise
|
||||||
|
function YouTubeQueue.sync_with_playlist()
|
||||||
|
if debug then
|
||||||
|
print("Syncing with internal playlist")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get the current playlist count
|
||||||
|
local count = mp.get_property_number("playlist-count")
|
||||||
|
if count == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clear our queue
|
||||||
|
video_queue = {}
|
||||||
|
|
||||||
|
-- Add each item from the playlist to our queue
|
||||||
|
for i = 0, count - 1 do
|
||||||
|
local url = mp.get_property(string.format("playlist/%d/filename", i))
|
||||||
|
if url then
|
||||||
|
if not is_file(url) then
|
||||||
|
local info = YouTubeQueue.get_video_info(url)
|
||||||
|
if info then
|
||||||
|
info["video_url"] = url
|
||||||
|
table.insert(video_queue, info)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local channel_url, video_name = split_path(url)
|
||||||
|
if not isnull(channel_url) and not isnull(video_name) then
|
||||||
|
table.insert(video_queue, {
|
||||||
|
video_url = url,
|
||||||
|
video_name = video_name,
|
||||||
|
channel_url = channel_url,
|
||||||
|
channel_name = "Local file",
|
||||||
|
thumbnail_url = "",
|
||||||
|
view_count = "",
|
||||||
|
upload_date = "",
|
||||||
|
category = "",
|
||||||
|
subscribers = "",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update current index
|
||||||
|
YouTubeQueue.update_current_index(false)
|
||||||
|
return true
|
||||||
|
end
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- LISTENERS {{{
|
-- LISTENERS {{{
|
||||||
@@ -988,10 +1157,8 @@ local function on_playback_restart()
|
|||||||
print("Playback restart event triggered.")
|
print("Playback restart event triggered.")
|
||||||
end
|
end
|
||||||
if current_video == nil then
|
if current_video == nil then
|
||||||
local url = mp.get_property("path")
|
-- Instead of just adding the current file, sync with the entire playlist
|
||||||
YouTubeQueue.add_to_queue(url)
|
YouTubeQueue.sync_with_playlist()
|
||||||
---@diagnostic disable-next-line: param-type-mismatch
|
|
||||||
YouTubeQueue.add_to_history_db(current_video)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1050,6 +1217,13 @@ 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)
|
mp.register_event("file-loaded", on_file_loaded)
|
||||||
|
mp.add_hook("on_load", 50, function()
|
||||||
|
if debug then
|
||||||
|
print("Startup hook triggered")
|
||||||
|
end
|
||||||
|
YouTubeQueue.sync_with_playlist()
|
||||||
|
end)
|
||||||
|
|
||||||
|
|
||||||
-- 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user