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:
2026-01-18 21:43:06 -08:00
parent 9d1b6d7eab
commit dd50f3eaad

View File

@@ -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,9 +1057,12 @@ 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*$")
if trimmed then
item = trimmed:gsub('"', "'")
table.insert(urls, item) 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)
end end
@@ -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)