summaryrefslogtreecommitdiff
path: root/mac/.config/mpv/scripts/playlistmanager.lua
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-08-23 12:42:37 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2025-08-23 12:42:37 +0900
commit07d294425a98ee5d1e22d03e2b24ae2c76e487c0 (patch)
treea6818f0d64438c5fdb88b00a35d944f80c056213 /mac/.config/mpv/scripts/playlistmanager.lua
parent6fc28cdb3529ca8ee864cb5c41674cb0a4af72a1 (diff)
updates
Diffstat (limited to 'mac/.config/mpv/scripts/playlistmanager.lua')
-rw-r--r--mac/.config/mpv/scripts/playlistmanager.lua1755
1 files changed, 1755 insertions, 0 deletions
diff --git a/mac/.config/mpv/scripts/playlistmanager.lua b/mac/.config/mpv/scripts/playlistmanager.lua
new file mode 100644
index 0000000..a0e20ed
--- /dev/null
+++ b/mac/.config/mpv/scripts/playlistmanager.lua
@@ -0,0 +1,1755 @@
+local settings = {
+
+ -- #### FUNCTIONALITY SETTINGS
+
+ --navigation keybindings force override only while playlist is visible
+ --if "no" then you can display the playlist by any of the navigation keys
+ dynamic_binds = true,
+
+ -- to bind multiple keys separate them by a space
+
+ -- main key to show playlist
+ key_showplaylist = "v",
+
+ -- display playlist while key is held down
+ key_peek_at_playlist = "",
+
+ -- dynamic keys
+ key_moveup = "k",
+ key_movedown = "j",
+ key_movepageup = "ctrl+u",
+ key_movepagedown = "ctrl+d",
+ key_movebegin = "g",
+ key_moveend = "G",
+ key_selectfile = "space",
+ key_unselectfile = "ctrl+space",
+ key_playfile = "ENTER",
+ key_removefile = "DEL",
+ key_closeplaylist = "ESC q ctrl+c",
+
+ -- extra functionality keys
+ key_sortplaylist = "shift+s",
+ key_shuffleplaylist = "ctrl+s",
+ key_reverseplaylist = "BS",
+ key_loadfiles = "alt+r",
+ key_saveplaylist = "INS",
+
+ --replaces matches on filenames based on extension, put as empty string to not replace anything
+ --replace rules are executed in provided order
+ --replace rule key is the pattern and value is the replace value
+ --uses :gsub('pattern', 'replace'), read more http://lua-users.org/wiki/StringLibraryTutorial
+ --'all' will match any extension or protocol if it has one
+ --uses json and parses it into a lua table to be able to support .conf file
+
+ filename_replace = [[
+ [
+ {
+ "protocol": { "all": true },
+ "rules": [
+ { "%%(%x%x)": "hex_to_char" }
+ ]
+ }
+ ]
+ ]],
+
+ --[=====[ START OF SAMPLE REPLACE - Remove this line to use it
+ --Sample replace: replaces underscore to space on all files
+ --for mp4 and webm; remove extension, remove brackets and surrounding whitespace, change dot between alphanumeric to space
+ filename_replace = [[
+ [
+ {
+ "ext": { "all": true},
+ "rules": [
+ { "_" : " " }
+ ]
+ },{
+ "ext": { "mp4": true, "mkv": true },
+ "rules": [
+ { "^(.+)%..+$": "%1" },
+ { "%s*[%[%(].-[%]%)]%s*": "" },
+ { "(%w)%.(%w)": "%1 %2" }
+ ]
+ },{
+ "protocol": { "http": true, "https": true },
+ "rules": [
+ { "^%a+://w*%.?": "" }
+ ]
+ }
+ ]
+ ]],
+--END OF SAMPLE REPLACE ]=====]
+
+ --json array of filetypes to search from directory
+ loadfiles_filetypes = [[
+ [
+ "jpg", "jpeg", "png", "tif", "tiff", "gif", "webp", "svg", "bmp",
+ "mp3", "wav", "ogm", "flac", "m4a", "wma", "ogg", "opus",
+ "mkv", "avi", "mp4", "ogv", "webm", "rmvb", "flv", "wmv", "mpeg", "mpg", "m4v", "3gp"
+ ]
+ ]],
+
+ --loadfiles at startup if 1 or more items in playlist
+ loadfiles_on_start = false,
+ -- loadfiles from working directory on idle startup
+ loadfiles_on_idle_start = false,
+ --always put loaded files after currently playing file
+ loadfiles_always_append = false,
+
+ --sort playlist when files are added to playlist
+ sortplaylist_on_file_add = false,
+
+ --default sorting method, must be one of: "name-asc", "name-desc", "date-asc", "date-desc", "size-asc", "size-desc".
+ default_sort = "name-asc",
+
+ --"linux | windows | auto"
+ system = "auto",
+
+ --Use ~ for home directory. Leave as empty to use mpv/playlists
+ playlist_savepath = "",
+
+ -- constant filename to save playlist as. Note that it will override existing playlist. Leave empty for generated name.
+ playlist_save_filename = "",
+
+ --save playlist automatically after current file was unloaded
+ save_playlist_on_file_end = false,
+
+ --show file title every time a new file is loaded
+ show_title_on_file_load = false,
+ --show playlist every time a new file is loaded
+ show_playlist_on_file_load = false,
+ --close playlist when selecting file to play
+ close_playlist_on_playfile = false,
+
+ --sync cursor when file is loaded from outside reasons(file-ending, playlist-next shortcut etc.)
+ --has the sideeffect of moving cursor if file happens to change when navigating
+ --good side is cursor always following current file when going back and forth files with playlist-next/prev
+ sync_cursor_on_load = true,
+
+ --allow the playlist cursor to loop from end to start and vice versa
+ loop_cursor = true,
+
+ --youtube-dl executable for title resolving if enabled, probably "youtube-dl" or "yt-dlp", can be absolute path
+ youtube_dl_executable = "yt-dlp",
+
+ -- allow playlistmanager to write watch later config when navigating between files
+ allow_write_watch_later_config = true,
+
+ -- reset cursor navigation when closing or opening playlist
+ reset_cursor_on_close = true,
+ reset_cursor_on_open = true,
+
+ --#### VISUAL SETTINGS
+
+ --prefer to display titles for following files: "all", "url", "none". Sorting still uses filename.
+ prefer_titles = "url",
+
+ --call youtube-dl to resolve the titles of urls in the playlist
+ resolve_url_titles = false,
+
+ --call ffprobe to resolve the titles of local files in the playlist (if they exist in the metadata)
+ resolve_local_titles = false,
+
+ -- timeout in seconds for url title resolving
+ resolve_title_timeout = 15,
+
+ -- how many url titles can be resolved at a time. Higher number might lead to stutters.
+ concurrent_title_resolve_limit = 10,
+
+ --osd timeout on inactivity in seconds, use 0 for no timeout
+ playlist_display_timeout = 0,
+
+ -- when peeking at playlist, show playlist at the very least for display timeout
+ peek_respect_display_timeout = false,
+
+ -- the maximum amount of lines playlist will render. -1 will automatically calculate lines.
+ showamount = -1,
+
+ --playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua
+ --example {\\q2\\an7\\fnUbuntu\\fs10\\b0\\bord1} equals: line-wrap=no, align=top left, font=Ubuntu, size=10, bold=no, border=1
+ --read http://docs.aegisub.org/3.2/ASS_Tags/ for reference of tags
+ --undeclared tags will use default osd settings
+ --these styles will be used for the whole playlist
+ --\\q2 style is recommended since filename wrapping may lead to unexpected rendering
+ --\\an7 style is recommended to align to top left otherwise, osd-align-x/y is respected
+ style_ass_tags = "{\\q2\\an7\\fnUbuntu\\fs20\\b0\\bord1\\c&HFFFFFF&}",
+
+ --paddings for left right and top bottom, depends on alignment
+ text_padding_x = 30,
+ text_padding_y = 60,
+
+ --screen dim when menu is open 0.0 - 1.0 (0 is no dim, 1 is black)
+ curtain_opacity = 0.0,
+
+ --set title of window with stripped name
+ set_title_stripped = false,
+ title_prefix = "",
+ title_suffix = " - mpv",
+
+ --slice long filenames, and how many chars to show
+ slice_longfilenames = false,
+ slice_longfilenames_amount = 70,
+
+ --Playlist header template
+ --%mediatitle or %filename = title or name of playing file
+ --%pos = position of playing file
+ --%cursor = position of navigation
+ --%plen = playlist length
+ --%N = newline
+ playlist_header = "[%cursor/%plen]",
+
+ --Playlist file templates
+ --%pos = position of file with leading zeros
+ --%name = title or name of file
+ --%N = newline
+ --you can also use the ass tags mentioned above. For example:
+ -- selected_file="{\\c&HFF00FF&}➔ %name" | to add a color for selected file. However, if you
+ -- use ass tags you need to reset them for every line (see https://github.com/jonniek/mpv-playlistmanager/issues/20)
+ normal_file = "{\\c&HFFFFFF&}○ %name",
+ hovered_file = "{\\c&H0080FF&}● %name",
+ selected_file = "{\\c&H00FF00&}➔ %name",
+ playing_file = "{\\c&HFF00FF&}▷ %name",
+ playing_hovered_file = "{\\c&H7F40FF&}▶ %name",
+ playing_selected_file = "{\\c&H00BF7F&}➤ %name",
+
+ -- what to show when playlist is truncated
+ playlist_sliced_prefix = "...",
+ playlist_sliced_suffix = "...",
+
+ --output visual feedback to OSD for tasks
+ display_osd_feedback = true,
+}
+local opts = require("mp.options")
+opts.read_options(settings, "playlistmanager", function(list)
+ update_opts(list)
+end)
+
+local utils = require("mp.utils")
+local msg = require("mp.msg")
+local assdraw = require("mp.assdraw")
+
+local alignment_table = {
+ [1] = { ["x"] = "left", ["y"] = "bottom" },
+ [2] = { ["x"] = "center", ["y"] = "bottom" },
+ [3] = { ["x"] = "right", ["y"] = "bottom" },
+ [4] = { ["x"] = "left", ["y"] = "center" },
+ [5] = { ["x"] = "center", ["y"] = "center" },
+ [6] = { ["x"] = "right", ["y"] = "center" },
+ [7] = { ["x"] = "left", ["y"] = "top" },
+ [8] = { ["x"] = "center", ["y"] = "top" },
+ [9] = { ["x"] = "right", ["y"] = "top" },
+}
+
+--check os
+if settings.system == "auto" then
+ local o = {}
+ if mp.get_property_native("options/vo-mmcss-profile", o) ~= o then
+ settings.system = "windows"
+ else
+ settings.system = "linux"
+ end
+end
+
+-- auto calculate showamount
+if settings.showamount == -1 then
+ -- same as draw_playlist() height
+ local h = 720
+
+ local playlist_h = h
+ -- both top and bottom with same padding
+ playlist_h = playlist_h - settings.text_padding_y * 2
+
+ -- osd-font-size is based on 720p height
+ -- see https://mpv.io/manual/stable/#options-osd-font-size
+ -- details in https://mpv.io/manual/stable/#options-sub-font-size
+ -- draw_playlist() is based on 720p, need some conversion
+ local fs = mp.get_property_native("osd-font-size") * h / 720
+ -- get the ass font size
+ if settings.style_ass_tags ~= nil then
+ local ass_fs_tag = settings.style_ass_tags:match("\\fs%d+")
+ if ass_fs_tag ~= nil then
+ fs = tonumber(ass_fs_tag:match("%d+"))
+ end
+ end
+
+ settings.showamount = math.floor(playlist_h / fs)
+
+ -- exclude the header line
+ if settings.playlist_header ~= "" then
+ settings.showamount = settings.showamount - 1
+ -- probably some newlines (%N or \N) in the header
+ for _ in settings.playlist_header:gmatch("%%N") do
+ settings.showamount = settings.showamount - 1
+ end
+ for _ in settings.playlist_header:gmatch("\\N") do
+ settings.showamount = settings.showamount - 1
+ end
+ end
+
+ msg.info("auto showamount: " .. settings.showamount)
+end
+
+--global variables
+local playlist_overlay = mp.create_osd_overlay("ass-events")
+local playlist_visible = false
+local strippedname = nil
+local path = nil
+local directory = nil
+local filename = nil
+local pos = 0
+local plen = 0
+local cursor = 0
+--table for saved media titles for later if we prefer them
+local title_table = {}
+-- table for urls and local file paths that we have requested to be resolved to titles
+local requested_titles = {}
+
+local filetype_lookup = {}
+
+function refresh_UI()
+ if not playlist_visible then
+ return
+ end
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ draw_playlist()
+end
+
+function update_opts(changelog)
+ msg.verbose("updating options")
+
+ --parse filename json
+ if changelog.filename_replace then
+ if settings.filename_replace ~= "" then
+ settings.filename_replace = utils.parse_json(settings.filename_replace)
+ else
+ settings.filename_replace = false
+ end
+ end
+
+ --parse loadfiles json
+ if changelog.loadfiles_filetypes then
+ settings.loadfiles_filetypes = utils.parse_json(settings.loadfiles_filetypes)
+
+ filetype_lookup = {}
+ --create loadfiles set
+ for _, ext in ipairs(settings.loadfiles_filetypes) do
+ filetype_lookup[ext] = true
+ end
+ end
+
+ if changelog.resolve_url_titles then
+ resolve_titles()
+ end
+
+ if changelog.resolve_local_titles then
+ resolve_titles()
+ end
+
+ if changelog.playlist_display_timeout then
+ keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+ keybindstimer:kill()
+ end
+
+ refresh_UI()
+end
+
+update_opts({ filename_replace = true, loadfiles_filetypes = true })
+
+----- winapi start -----
+-- in windows system, we can use the sorting function provided by the win32 API
+-- see https://learn.microsoft.com/en-us/windows/win32/api/shlwapi/nf-shlwapi-strcmplogicalw
+local winapisort = nil
+if settings.system == "windows" then
+ -- ffiok is false usually means the mpv builds without luajit
+ local ffiok, ffi = pcall(require, "ffi")
+ if ffiok then
+ ffi.cdef([[
+ int MultiByteToWideChar(unsigned int CodePage, unsigned long dwFlags, const char *lpMultiByteStr, int cbMultiByte, wchar_t *lpWideCharStr, int cchWideChar);
+ int StrCmpLogicalW(const wchar_t * psz1, const wchar_t * psz2);
+ ]])
+
+ local shlwapi = ffi.load("shlwapi.dll")
+
+ function MultiByteToWideChar(MultiByteStr)
+ local UTF8_CODEPAGE = 65001
+ if MultiByteStr then
+ local utf16_len = ffi.C.MultiByteToWideChar(UTF8_CODEPAGE, 0, MultiByteStr, -1, nil, 0)
+ if utf16_len > 0 then
+ local utf16_str = ffi.new("wchar_t[?]", utf16_len)
+ if ffi.C.MultiByteToWideChar(UTF8_CODEPAGE, 0, MultiByteStr, -1, utf16_str, utf16_len) > 0 then
+ return utf16_str
+ end
+ end
+ end
+ return ""
+ end
+
+ winapisort = function(a, b)
+ return shlwapi.StrCmpLogicalW(MultiByteToWideChar(a), MultiByteToWideChar(b)) < 0
+ end
+ end
+end
+----- winapi end -----
+
+local sort_modes = {
+ {
+ id = "name-asc",
+ title = "name ascending",
+ sort_fn = function(a, b, playlist)
+ if winapisort ~= nil then
+ return winapisort(playlist[a].string, playlist[b].string)
+ end
+ return alphanumsort(playlist[a].string, playlist[b].string)
+ end,
+ },
+ {
+ id = "name-desc",
+ title = "name descending",
+ sort_fn = function(a, b, playlist)
+ if winapisort ~= nil then
+ return winapisort(playlist[b].string, playlist[a].string)
+ end
+ return alphanumsort(playlist[b].string, playlist[a].string)
+ end,
+ },
+ {
+ id = "date-asc",
+ title = "date ascending",
+ sort_fn = function(a, b)
+ return (get_file_info(a).mtime or 0) < (get_file_info(b).mtime or 0)
+ end,
+ },
+ {
+ id = "date-desc",
+ title = "date descending",
+ sort_fn = function(a, b)
+ return (get_file_info(a).mtime or 0) > (get_file_info(b).mtime or 0)
+ end,
+ },
+ {
+ id = "size-asc",
+ title = "size ascending",
+ sort_fn = function(a, b)
+ return (get_file_info(a).size or 0) < (get_file_info(b).size or 0)
+ end,
+ },
+ {
+ id = "size-desc",
+ title = "size descending",
+ sort_fn = function(a, b)
+ return (get_file_info(a).size or 0) > (get_file_info(b).size or 0)
+ end,
+ },
+}
+
+local sort_mode = 1
+for mode, sort_data in pairs(sort_modes) do
+ if sort_data.id == settings.default_sort then
+ sort_mode = mode
+ end
+end
+
+function is_protocol(path)
+ return type(path) == "string" and path:match("^%a[%a%d-_]+://") ~= nil
+end
+
+function on_file_loaded()
+ refresh_globals()
+ if settings.sync_cursor_on_load then
+ cursor = pos
+ end
+ refresh_UI() -- refresh only after moving cursor
+
+ filename = mp.get_property("filename")
+ path = mp.get_property("path")
+ local media_title = mp.get_property("media-title")
+ if is_protocol(path) and not title_table[path] and path ~= media_title then
+ title_table[path] = media_title
+ end
+
+ strippedname = stripfilename(mp.get_property("media-title"))
+ if settings.show_title_on_file_load then
+ mp.commandv("show-text", strippedname)
+ end
+ if settings.show_playlist_on_file_load then
+ showplaylist()
+ end
+ if settings.set_title_stripped then
+ mp.set_property("title", settings.title_prefix .. strippedname .. settings.title_suffix)
+ end
+end
+
+function on_start_file()
+ refresh_globals()
+ filename = mp.get_property("filename")
+ path = mp.get_property("path")
+ --if not a url then join path with working directory
+ if not is_protocol(path) then
+ path = utils.join_path(mp.get_property("working-directory"), path)
+ directory = utils.split_path(path)
+ else
+ directory = nil
+ end
+
+ if settings.loadfiles_on_start and plen == 1 then
+ local ext = filename:match("%.([^%.]+)$")
+ -- a directory or playlist has been loaded, let's not do anything as mpv will expand it into files
+ if ext and filetype_lookup[ext:lower()] then
+ msg.info("Loading files from playing files directory")
+ playlist()
+ end
+ end
+end
+
+function on_end_file()
+ if settings.save_playlist_on_file_end then
+ save_playlist()
+ end
+ strippedname = nil
+ path = nil
+ directory = nil
+ filename = nil
+end
+
+function refresh_globals()
+ pos = mp.get_property_number("playlist-pos", 0)
+ plen = mp.get_property_number("playlist-count", 0)
+end
+
+function escapepath(dir, escapechar)
+ return string.gsub(dir, escapechar, "\\" .. escapechar)
+end
+
+function replace_table_has_value(value, valid_values)
+ if value == nil or valid_values == nil then
+ return false
+ end
+ return valid_values["all"] or valid_values[value]
+end
+
+local filename_replace_functions = {
+ --decode special characters in url
+ hex_to_char = function(x)
+ return string.char(tonumber(x, 16))
+ end,
+}
+
+--strip a filename based on its extension or protocol according to rules in settings
+function stripfilename(pathfile, media_title)
+ if pathfile == nil then
+ return ""
+ end
+ local ext = pathfile:match("%.([^%.]+)$")
+ local protocol = pathfile:match("^(%a%a+)://")
+ if not ext then
+ ext = ""
+ end
+ local tmp = pathfile
+ if settings.filename_replace and not media_title then
+ for k, v in ipairs(settings.filename_replace) do
+ if replace_table_has_value(ext, v["ext"]) or replace_table_has_value(protocol, v["protocol"]) then
+ for ruleindex, indexrules in ipairs(v["rules"]) do
+ for rule, override in pairs(indexrules) do
+ override = filename_replace_functions[override] or override
+ tmp = tmp:gsub(rule, override)
+ end
+ end
+ end
+ end
+ end
+ if settings.slice_longfilenames and tmp:len() > settings.slice_longfilenames_amount + 5 then
+ tmp = tmp:sub(1, settings.slice_longfilenames_amount) .. " ..."
+ end
+ return tmp
+end
+
+--gets the file info of an item
+function get_file_info(item)
+ local path = mp.get_property("playlist/" .. item - 1 .. "/filename")
+ if is_protocol(path) then
+ return {}
+ end
+ local file_info = utils.file_info(path)
+ if not file_info then
+ msg.warn("failed to read file info for", path)
+ return {}
+ end
+
+ return file_info
+end
+
+--gets a nicename of playlist entry at 0-based position i
+function get_name_from_index(i, notitle)
+ refresh_globals()
+ if plen <= i then
+ msg.error("no index in playlist", i, "length", plen)
+ return nil
+ end
+ local _, name = nil
+ local title = mp.get_property("playlist/" .. i .. "/title")
+ local name = mp.get_property("playlist/" .. i .. "/filename")
+
+ local should_use_title = settings.prefer_titles == "all" or is_protocol(name) and settings.prefer_titles == "url"
+ --check if file has a media title stored or as property
+ if not title and should_use_title then
+ local mtitle = mp.get_property("media-title")
+ if i == pos and mp.get_property("filename") ~= mtitle then
+ if not title_table[name] then
+ title_table[name] = mtitle
+ end
+ title = mtitle
+ elseif title_table[name] then
+ title = title_table[name]
+ end
+ end
+
+ --if we have media title use a more conservative strip
+ if title and not notitle and should_use_title then
+ -- Escape a string for verbatim display on the OSD
+ -- Ref: https://github.com/mpv-player/mpv/blob/94677723624fb84756e65c8f1377956667244bc9/player/lua/stats.lua#L145
+ return stripfilename(title, true):gsub("\\", "\\\239\187\191"):gsub("{", "\\{"):gsub("^ ", "\\h")
+ end
+
+ --remove paths if they exist, keeping protocols for stripping
+ if string.sub(name, 1, 1) == "/" or name:match("^%a:[/\\]") then
+ _, name = utils.split_path(name)
+ end
+ return stripfilename(name):gsub("\\", "\\\239\187\191"):gsub("{", "\\{"):gsub("^ ", "\\h")
+end
+
+function parse_header(string)
+ local esc_title = stripfilename(mp.get_property("media-title"), true):gsub("%%", "%%%%")
+ local esc_file = stripfilename(mp.get_property("filename")):gsub("%%", "%%%%")
+ return string
+ :gsub("%%N", "\\N")
+ -- add a blank character at the end of each '\N' to ensure that the height of the empty line is the same as the non empty line
+ :gsub(
+ "\\N",
+ "\\N "
+ )
+ :gsub("%%pos", mp.get_property_number("playlist-pos", 0) + 1)
+ :gsub("%%plen", mp.get_property("playlist-count"))
+ :gsub("%%cursor", cursor + 1)
+ :gsub("%%mediatitle", esc_title)
+ :gsub("%%filename", esc_file)
+ -- undo name escape
+ :gsub("%%%%", "%%")
+end
+
+function parse_filename(string, name, index)
+ local base = tostring(plen):len()
+ local esc_name = stripfilename(name):gsub("%%", "%%%%")
+ return string
+ :gsub("%%N", "\\N")
+ :gsub("%%pos", string.format("%0" .. base .. "d", index + 1))
+ :gsub("%%name", esc_name)
+ -- undo name escape
+ :gsub("%%%%", "%%")
+end
+
+function parse_filename_by_index(index)
+ local template = settings.normal_file
+
+ local is_idle = mp.get_property_native("idle-active")
+ local position = is_idle and -1 or pos
+
+ if index == position then
+ if index == cursor then
+ if selection then
+ template = settings.playing_selected_file
+ else
+ template = settings.playing_hovered_file
+ end
+ else
+ template = settings.playing_file
+ end
+ elseif index == cursor then
+ if selection then
+ template = settings.selected_file
+ else
+ template = settings.hovered_file
+ end
+ end
+
+ return parse_filename(template, get_name_from_index(index), index)
+end
+
+function draw_playlist()
+ refresh_globals()
+ local ass = assdraw.ass_new()
+
+ local _, _, a = mp.get_osd_size()
+ local h = 720
+ local w = math.ceil(h * a)
+
+ if settings.curtain_opacity ~= nil and settings.curtain_opacity ~= 0 and settings.curtain_opacity <= 1.0 then
+ -- curtain dim from https://github.com/christoph-heinrich/mpv-quality-menu/blob/501794bfbef468ee6a61e54fc8821fe5cd72c4ed/quality-menu.lua#L699-L707
+ local alpha = 255 - math.ceil(255 * settings.curtain_opacity)
+ ass.text = string.format("{\\pos(0,0)\\rDefault\\an7\\1c&H000000&\\alpha&H%X&}", alpha)
+ ass:draw_start()
+ ass:rect_cw(0, 0, w, h)
+ ass:draw_stop()
+ ass:new_event()
+ end
+
+ ass:append(settings.style_ass_tags)
+
+ -- align from mpv.conf
+ local align_x = mp.get_property("osd-align-x")
+ local align_y = mp.get_property("osd-align-y")
+ -- align from style_ass_tags
+ if settings.style_ass_tags ~= nil then
+ local an = tonumber(settings.style_ass_tags:match("\\an(%d)"))
+ if an ~= nil and alignment_table[an] ~= nil then
+ align_x = alignment_table[an]["x"]
+ align_y = alignment_table[an]["y"]
+ end
+ end
+ -- range of x [0, w-1]
+ local pos_x
+ if align_x == "left" then
+ pos_x = settings.text_padding_x
+ elseif align_x == "right" then
+ pos_x = w - 1 - settings.text_padding_x
+ else
+ pos_x = math.floor((w - 1) / 2)
+ end
+ -- range of y [0, h-1]
+ local pos_y
+ if align_y == "top" then
+ pos_y = settings.text_padding_y
+ elseif align_y == "bottom" then
+ pos_y = h - 1 - settings.text_padding_y
+ else
+ pos_y = math.floor((h - 1) / 2)
+ end
+ ass:pos(pos_x, pos_y)
+
+ if settings.playlist_header ~= "" then
+ ass:append(parse_header(settings.playlist_header) .. "\\N")
+ end
+
+ -- (visible index, playlist index) pairs of playlist entries that should be rendered
+ local visible_indices = {}
+
+ local one_based_cursor = cursor + 1
+ table.insert(visible_indices, one_based_cursor)
+
+ local offset = 1
+ local visible_indices_length = 1
+ while visible_indices_length < settings.showamount and visible_indices_length < plen do
+ -- add entry for offset steps below the cursor
+ local below = one_based_cursor + offset
+ if below <= plen then
+ table.insert(visible_indices, below)
+ visible_indices_length = visible_indices_length + 1
+ end
+
+ -- add entry for offset steps above the cursor
+ -- also need to double check that there is still space, this happens if we have even numbered limit
+ local above = one_based_cursor - offset
+ if above >= 1 and visible_indices_length < settings.showamount and visible_indices_length < plen then
+ table.insert(visible_indices, 1, above)
+ visible_indices_length = visible_indices_length + 1
+ end
+
+ offset = offset + 1
+ end
+
+ -- both indices are 1 based
+ for display_index, playlist_index in pairs(visible_indices) do
+ if display_index == 1 and playlist_index ~= 1 then
+ ass:append(settings.playlist_sliced_prefix .. "\\N")
+ elseif display_index == settings.showamount and playlist_index ~= plen then
+ ass:append(settings.playlist_sliced_suffix)
+ else
+ -- parse_filename_by_index expects 0 based index
+ ass:append(parse_filename_by_index(playlist_index - 1) .. "\\N")
+ end
+ end
+
+ playlist_overlay.data = ass.text
+ playlist_overlay:update()
+end
+
+local peek_display_timer = nil
+local peek_button_pressed = false
+
+function peek_timeout()
+ peek_display_timer:kill()
+ if not peek_button_pressed and not playlist_visible then
+ remove_keybinds()
+ end
+end
+
+function handle_complex_playlist_toggle(table)
+ local event = table["event"]
+ if event == "press" then
+ msg.error("Complex key event not supported. Falling back to normal playlist display.")
+ showplaylist()
+ elseif event == "down" then
+ showplaylist(1000000)
+ if settings.peek_respect_display_timeout then
+ peek_button_pressed = true
+ peek_display_timer = mp.add_periodic_timer(settings.playlist_display_timeout, peek_timeout)
+ end
+ elseif event == "up" then
+ -- set playlist state to not visible, doesn't actually hide playlist yet
+ -- this will allow us to check if other functionality has rendered playlist before removing binds
+ playlist_visible = false
+
+ function remove_keybinds_after_timeout()
+ -- if playlist is still not visible then lets actually hide it
+ -- this lets other keys that interupt the peek to render playlist without peek up event closing it
+ if not playlist_visible then
+ remove_keybinds()
+ end
+ end
+
+ if settings.peek_respect_display_timeout then
+ peek_button_pressed = false
+ if not peek_display_timer:is_enabled() then
+ mp.add_timeout(0.01, remove_keybinds_after_timeout)
+ end
+ else
+ -- use small delay to let dynamic binds run before keys are potentially unbound
+ mp.add_timeout(0.01, remove_keybinds_after_timeout)
+ end
+ end
+end
+
+function toggle_playlist(show_function)
+ local show = show_function or showplaylist
+ if playlist_visible then
+ remove_keybinds()
+ else
+ -- toggle always shows without timeout
+ show(0)
+ end
+end
+
+function showplaylist(duration)
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ if not playlist_visible and settings.reset_cursor_on_open then
+ resetcursor()
+ end
+
+ playlist_visible = true
+ add_keybinds()
+
+ draw_playlist()
+ keybindstimer:kill()
+
+ local dur = tonumber(duration) or settings.playlist_display_timeout
+ if dur > 0 then
+ keybindstimer = mp.add_periodic_timer(dur, remove_keybinds)
+ end
+end
+
+function showplaylist_non_interactive(duration)
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ if not playlist_visible and settings.reset_cursor_on_open then
+ resetcursor()
+ end
+ playlist_visible = true
+ draw_playlist()
+ keybindstimer:kill()
+
+ local dur = tonumber(duration) or settings.playlist_display_timeout
+ if dur > 0 then
+ keybindstimer = mp.add_periodic_timer(dur, remove_keybinds)
+ end
+end
+
+selection = nil
+function selectfile()
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ if not selection then
+ selection = cursor
+ else
+ selection = nil
+ end
+ showplaylist()
+end
+
+function unselectfile()
+ selection = nil
+ showplaylist()
+end
+
+function resetcursor()
+ selection = nil
+ cursor = mp.get_property_number("playlist-pos", 1)
+end
+
+function removefile()
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ selection = nil
+ if cursor == pos then
+ mp.command('script-message unseenplaylist mark true "playlistmanager avoid conflict when removing file"')
+ end
+ mp.commandv("playlist-remove", cursor)
+ if cursor == plen - 1 then
+ cursor = cursor - 1
+ end
+ if plen == 1 then
+ remove_keybinds()
+ else
+ showplaylist()
+ end
+end
+
+function moveup()
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ if cursor ~= 0 then
+ if selection then
+ mp.commandv("playlist-move", cursor, cursor - 1)
+ end
+ cursor = cursor - 1
+ elseif settings.loop_cursor then
+ if selection then
+ mp.commandv("playlist-move", cursor, plen)
+ end
+ cursor = plen - 1
+ end
+ showplaylist()
+end
+
+function movedown()
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ if cursor ~= plen - 1 then
+ if selection then
+ mp.commandv("playlist-move", cursor, cursor + 2)
+ end
+ cursor = cursor + 1
+ elseif settings.loop_cursor then
+ if selection then
+ mp.commandv("playlist-move", cursor, 0)
+ end
+ cursor = 0
+ end
+ showplaylist()
+end
+
+function movepageup()
+ refresh_globals()
+ if plen == 0 or cursor == 0 then
+ return
+ end
+ local prev_cursor = cursor
+ cursor = cursor - settings.showamount
+ if cursor < 0 then
+ cursor = 0
+ end
+ if selection then
+ mp.commandv("playlist-move", prev_cursor, cursor)
+ end
+ showplaylist()
+end
+
+function movepagedown()
+ refresh_globals()
+ if plen == 0 or cursor == plen - 1 then
+ return
+ end
+ local prev_cursor = cursor
+ cursor = cursor + settings.showamount
+ if cursor >= plen then
+ cursor = plen - 1
+ end
+ if selection then
+ mp.commandv("playlist-move", prev_cursor, cursor + 1)
+ end
+ showplaylist()
+end
+
+function movebegin()
+ refresh_globals()
+ if plen == 0 or cursor == 0 then
+ return
+ end
+ local prev_cursor = cursor
+ cursor = 0
+ if selection then
+ mp.commandv("playlist-move", prev_cursor, cursor)
+ end
+ showplaylist()
+end
+
+function moveend()
+ refresh_globals()
+ if plen == 0 or cursor == plen - 1 then
+ return
+ end
+ local prev_cursor = cursor
+ cursor = plen - 1
+ if selection then
+ mp.commandv("playlist-move", prev_cursor, cursor + 1)
+ end
+ showplaylist()
+end
+
+function write_watch_later(force_write)
+ if settings.allow_write_watch_later_config then
+ if mp.get_property_bool("save-position-on-quit") or force_write then
+ mp.command("write-watch-later-config")
+ end
+ end
+end
+
+function playlist_next()
+ write_watch_later(true)
+ mp.commandv("playlist-next", "weak")
+ if settings.close_playlist_on_playfile then
+ remove_keybinds()
+ end
+ refresh_UI()
+end
+
+function playlist_prev()
+ write_watch_later(true)
+ mp.commandv("playlist-prev", "weak")
+ if settings.close_playlist_on_playfile then
+ remove_keybinds()
+ end
+ refresh_UI()
+end
+
+function playlist_random()
+ write_watch_later()
+ refresh_globals()
+ if plen < 2 then
+ return
+ end
+ math.randomseed(os.time())
+ local random = pos
+ while random == pos do
+ random = math.random(0, plen - 1)
+ end
+ mp.set_property("playlist-pos", random)
+ if settings.close_playlist_on_playfile then
+ remove_keybinds()
+ end
+end
+
+function playfile()
+ refresh_globals()
+ if plen == 0 then
+ return
+ end
+ selection = nil
+ local is_idle = mp.get_property_native("idle-active")
+ if cursor ~= pos or is_idle then
+ write_watch_later()
+ mp.set_property("playlist-pos", cursor)
+ else
+ if cursor ~= plen - 1 then
+ cursor = cursor + 1
+ end
+ write_watch_later()
+ mp.commandv("playlist-next", "weak")
+ end
+ if settings.close_playlist_on_playfile then
+ remove_keybinds()
+ elseif playlist_visible then
+ showplaylist()
+ end
+end
+
+function file_filter(filenames)
+ local files = {}
+ for i = 1, #filenames do
+ local file = filenames[i]
+ local ext = file:match("%.([^%.]+)$")
+ if ext and filetype_lookup[ext:lower()] then
+ table.insert(files, file)
+ end
+ end
+ return files
+end
+
+function get_playlist_filenames_set()
+ local filenames = {}
+ for n = 0, plen - 1, 1 do
+ local filename = mp.get_property("playlist/" .. n .. "/filename")
+ local _, file = utils.split_path(filename)
+ filenames[file] = true
+ end
+ return filenames
+end
+
+--Creates a playlist of all files in directory, will keep the order and position
+--For exaple, Folder has 12 files, you open the 5th file and run this, the remaining 7 are added behind the 5th file and prior 4 files before it
+function playlist(force_dir)
+ refresh_globals()
+ if not directory and plen > 0 then
+ return
+ end
+ local hasfile = true
+ if plen == 0 then
+ hasfile = false
+ dir = mp.get_property("working-directory")
+ else
+ dir = directory
+ end
+
+ if dir == "." then
+ dir = ""
+ end
+ if force_dir then
+ dir = force_dir
+ end
+
+ local files = file_filter(utils.readdir(dir, "files"))
+ if winapisort ~= nil then
+ table.sort(files, winapisort)
+ else
+ table.sort(files, alphanumsort)
+ end
+
+ if files == nil then
+ msg.verbose("no files in directory")
+ return
+ end
+
+ local filenames = get_playlist_filenames_set()
+ local c, c2 = 0, 0
+ if files then
+ local cur = false
+ local filename = mp.get_property("filename")
+ for _, file in ipairs(files) do
+ if file == nil or file[1] == "." then
+ break
+ end
+ local appendstr = "append"
+ if not hasfile then
+ cur = true
+ appendstr = "append-play"
+ hasfile = true
+ end
+ if filename == file then
+ cur = true
+ elseif filenames[file] then
+ -- skip files already in playlist
+ elseif cur == true or settings.loadfiles_always_append then
+ mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+ msg.info("Appended to playlist: " .. file)
+ c2 = c2 + 1
+ else
+ mp.commandv("loadfile", utils.join_path(dir, file), appendstr)
+ msg.info("Prepended to playlist: " .. file)
+ mp.commandv("playlist-move", mp.get_property_number("playlist-count", 1) - 1, c)
+ c = c + 1
+ end
+ end
+ if c2 > 0 or c > 0 then
+ msg.info("Added " .. c + c2 .. " files to playlist")
+ else
+ msg.info("No additional files found")
+ end
+ cursor = mp.get_property_number("playlist-pos", 1)
+ else
+ msg.error("Could not scan for files: " .. (error or ""))
+ end
+ refresh_globals()
+ if playlist_visible then
+ showplaylist()
+ end
+ if settings.display_osd_feedback then
+ if c2 > 0 or c > 0 then
+ mp.osd_message("Added " .. c + c2 .. " files to playlist")
+ else
+ mp.osd_message("No additional files found")
+ end
+ end
+ return c + c2
+end
+
+function parse_home(path)
+ if not path:find("^~") then
+ return path
+ end
+ local home_dir = os.getenv("HOME") or os.getenv("USERPROFILE")
+ if not home_dir then
+ local drive = os.getenv("HOMEDRIVE")
+ local path = os.getenv("HOMEPATH")
+ if drive and path then
+ home_dir = utils.join_path(drive, path)
+ else
+ msg.error("Couldn't find home dir.")
+ return nil
+ end
+ end
+ local result = path:gsub("^~", home_dir)
+ return result
+end
+
+local interactive_save = false
+function activate_playlist_save()
+ if interactive_save then
+ remove_keybinds()
+ mp.command('script-message playlistmanager-save-interactive "start interactive filenaming process"')
+ else
+ save_playlist()
+ end
+end
+
+--saves the current playlist into a m3u file
+function save_playlist(filename)
+ local length = mp.get_property_number("playlist-count", 0)
+ if length == 0 then
+ return
+ end
+
+ --get playlist save path
+ local savepath
+ if settings.playlist_savepath == nil or settings.playlist_savepath == "" then
+ savepath = mp.command_native({ "expand-path", "~~home/" }) .. "/playlists"
+ else
+ savepath = parse_home(settings.playlist_savepath)
+ if savepath == nil then
+ return
+ end
+ end
+
+ --create savepath if it doesn't exist
+ if utils.readdir(savepath) == nil then
+ local windows_args = { "powershell", "-NoProfile", "-Command", "mkdir", savepath }
+ local unix_args = { "mkdir", savepath }
+ local args = settings.system == "windows" and windows_args or unix_args
+ local res = utils.subprocess({ args = args, cancellable = false })
+ if res.status ~= 0 then
+ msg.error(
+ "Failed to create playlist save directory " .. savepath .. ". Error: " .. (res.error or "unknown")
+ )
+ return
+ end
+ end
+
+ local name = filename
+ if name == nil then
+ if settings.playlist_save_filename == nil or settings.playlist_save_filename == "" then
+ local date = os.date("*t")
+ local datestring = ("%02d-%02d-%02d_%02d-%02d-%02d"):format(
+ date.year,
+ date.month,
+ date.day,
+ date.hour,
+ date.min,
+ date.sec
+ )
+
+ name = datestring .. "_playlist-size_" .. length .. ".m3u"
+ else
+ name = settings.playlist_save_filename
+ end
+ end
+
+ local savepath = utils.join_path(savepath, name)
+ local file, err = io.open(savepath, "w")
+ if not file then
+ msg.error("Error in creating playlist file, check permissions. Error: " .. (err or "unknown"))
+ else
+ file:write("#EXTM3U\n")
+ local i = 0
+ while i < length do
+ local pwd = mp.get_property("working-directory")
+ local filename = mp.get_property("playlist/" .. i .. "/filename")
+ local fullpath = filename
+ if not is_protocol(filename) then
+ fullpath = utils.join_path(pwd, filename)
+ end
+ local title = mp.get_property("playlist/" .. i .. "/title") or title_table[filename]
+ if title then
+ file:write("#EXTINF:," .. title .. "\n")
+ end
+ file:write(fullpath, "\n")
+ i = i + 1
+ end
+ local saved_msg = "Playlist written to: " .. savepath
+ if settings.display_osd_feedback then
+ mp.osd_message(saved_msg)
+ end
+ msg.info(saved_msg)
+ file:close()
+ end
+end
+
+function alphanumsort(a, b)
+ local function padnum(d)
+ local dec, n = string.match(d, "(%.?)0*(.+)")
+ return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n)
+ end
+ return tostring(a):lower():gsub("%.?%d+", padnum) .. ("%3d"):format(#b)
+ < tostring(b):lower():gsub("%.?%d+", padnum) .. ("%3d"):format(#a)
+end
+
+-- fast sort algo from https://github.com/zsugabubus/dotfiles/blob/master/.config/mpv/scripts/playlist-filtersort.lua
+function sortplaylist(startover)
+ local playlist = mp.get_property_native("playlist")
+ if #playlist < 2 then
+ return
+ end
+
+ local order = {}
+ for i = 1, #playlist do
+ order[i] = i
+ playlist[i].string = get_name_from_index(i - 1, true)
+ end
+
+ table.sort(order, function(a, b)
+ return sort_modes[sort_mode].sort_fn(a, b, playlist)
+ end)
+
+ for i = 1, #playlist do
+ playlist[order[i]].new_pos = i
+ end
+
+ for i = 1, #playlist do
+ while true do
+ local j = playlist[i].new_pos
+ if i == j then
+ break
+ end
+ mp.commandv("playlist-move", i - 1, (j + 1) - 1)
+ mp.commandv("playlist-move", (j - 1) - 1, i - 1)
+ playlist[j], playlist[i] = playlist[i], playlist[j]
+ end
+ end
+
+ for i = 1, #playlist do
+ local filename = mp.get_property("playlist/" .. i - 1 .. "/filename")
+ local ext = filename:match("%.([^%.]+)$")
+ if not ext or not filetype_lookup[ext:lower()] then
+ --move the directory to the end of the playlist
+ mp.commandv("playlist-move", i - 1, #playlist)
+ end
+ end
+
+ cursor = mp.get_property_number("playlist-pos", 0)
+ if startover then
+ mp.set_property("playlist-pos", 0)
+ end
+ if playlist_visible then
+ showplaylist()
+ end
+ if settings.display_osd_feedback then
+ mp.osd_message("Playlist sorted with " .. sort_modes[sort_mode].title)
+ end
+end
+
+function reverseplaylist()
+ local length = mp.get_property_number("playlist-count", 0)
+ if length < 2 then
+ return
+ end
+ for outer = 1, length - 1, 1 do
+ mp.commandv("playlist-move", outer, 0)
+ end
+ if playlist_visible then
+ showplaylist()
+ end
+ if settings.display_osd_feedback then
+ mp.osd_message("Playlist reversed")
+ end
+end
+
+function shuffleplaylist()
+ refresh_globals()
+ if plen < 2 then
+ return
+ end
+ mp.command("playlist-shuffle")
+ math.randomseed(os.time())
+ mp.commandv("playlist-move", pos, math.random(0, plen - 1))
+
+ local playlist = mp.get_property_native("playlist")
+ for i = 1, #playlist do
+ local filename = mp.get_property("playlist/" .. i - 1 .. "/filename")
+ local ext = filename:match("%.([^%.]+)$")
+ if not ext or not filetype_lookup[ext:lower()] then
+ --move the directory to the end of the playlist
+ mp.commandv("playlist-move", i - 1, #playlist)
+ end
+ end
+
+ mp.set_property("playlist-pos", 0)
+ refresh_globals()
+ if playlist_visible then
+ showplaylist()
+ end
+ if settings.display_osd_feedback then
+ mp.osd_message("Playlist shuffled")
+ end
+end
+
+function bind_keys(keys, name, func, opts)
+ if keys == nil or keys == "" then
+ mp.add_key_binding(keys, name, func, opts)
+ return
+ end
+ local i = 1
+ for key in keys:gmatch("[^%s]+") do
+ local prefix = i == 1 and "" or i
+ mp.add_key_binding(key, name .. prefix, func, opts)
+ i = i + 1
+ end
+end
+
+function bind_keys_forced(keys, name, func, opts)
+ if keys == nil or keys == "" then
+ mp.add_forced_key_binding(keys, name, func, opts)
+ return
+ end
+ local i = 1
+ for key in keys:gmatch("[^%s]+") do
+ local prefix = i == 1 and "" or i
+ mp.add_forced_key_binding(key, name .. prefix, func, opts)
+ i = i + 1
+ end
+end
+
+function unbind_keys(keys, name)
+ if keys == nil or keys == "" then
+ mp.remove_key_binding(name)
+ return
+ end
+ local i = 1
+ for key in keys:gmatch("[^%s]+") do
+ local prefix = i == 1 and "" or i
+ mp.remove_key_binding(name .. prefix)
+ i = i + 1
+ end
+end
+
+function add_keybinds()
+ bind_keys_forced(settings.key_moveup, "moveup", moveup, "repeatable")
+ bind_keys_forced(settings.key_movedown, "movedown", movedown, "repeatable")
+ bind_keys_forced(settings.key_movepageup, "movepageup", movepageup, "repeatable")
+ bind_keys_forced(settings.key_movepagedown, "movepagedown", movepagedown, "repeatable")
+ bind_keys_forced(settings.key_movebegin, "movebegin", movebegin, "repeatable")
+ bind_keys_forced(settings.key_moveend, "moveend", moveend, "repeatable")
+ bind_keys_forced(settings.key_selectfile, "selectfile", selectfile)
+ bind_keys_forced(settings.key_unselectfile, "unselectfile", unselectfile)
+ bind_keys_forced(settings.key_playfile, "playfile", playfile)
+ bind_keys_forced(settings.key_removefile, "removefile", removefile, "repeatable")
+ bind_keys_forced(settings.key_closeplaylist, "closeplaylist", remove_keybinds)
+end
+
+function remove_keybinds()
+ keybindstimer:kill()
+ keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+ keybindstimer:kill()
+ playlist_overlay.data = ""
+ playlist_overlay:update()
+ playlist_visible = false
+ if settings.reset_cursor_on_close then
+ resetcursor()
+ end
+ if settings.dynamic_binds then
+ unbind_keys(settings.key_moveup, "moveup")
+ unbind_keys(settings.key_movedown, "movedown")
+ unbind_keys(settings.key_movepageup, "movepageup")
+ unbind_keys(settings.key_movepagedown, "movepagedown")
+ unbind_keys(settings.key_movebegin, "movebegin")
+ unbind_keys(settings.key_moveend, "moveend")
+ unbind_keys(settings.key_selectfile, "selectfile")
+ unbind_keys(settings.key_unselectfile, "unselectfile")
+ unbind_keys(settings.key_playfile, "playfile")
+ unbind_keys(settings.key_removefile, "removefile")
+ unbind_keys(settings.key_closeplaylist, "closeplaylist")
+ end
+end
+
+keybindstimer = mp.add_periodic_timer(settings.playlist_display_timeout, remove_keybinds)
+keybindstimer:kill()
+
+if not settings.dynamic_binds then
+ add_keybinds()
+end
+
+if settings.loadfiles_on_idle_start and mp.get_property_number("playlist-count", 0) == 0 then
+ playlist()
+end
+
+mp.observe_property("playlist-count", "number", function(_, plcount)
+ --if we promised to listen and sort on playlist size increase do it
+ if settings.sortplaylist_on_file_add and (plcount > plen) then
+ msg.info("Added files will be automatically sorted")
+ refresh_globals()
+ sortplaylist()
+ end
+ refresh_UI()
+ resolve_titles()
+end)
+
+url_request_queue = {}
+function url_request_queue.push(item)
+ table.insert(url_request_queue, item)
+end
+function url_request_queue.pop()
+ return table.remove(url_request_queue, 1)
+end
+local url_titles_to_fetch = url_request_queue
+local ongoing_url_requests = {}
+
+function url_fetching_throttler()
+ if #url_titles_to_fetch == 0 then
+ url_title_fetch_timer:kill()
+ end
+
+ local ongoing_url_requests_count = 0
+ for _, ongoing in pairs(ongoing_url_requests) do
+ if ongoing then
+ ongoing_url_requests_count = ongoing_url_requests_count + 1
+ end
+ end
+
+ -- start resolving some url titles if there is available slots
+ local amount_to_fetch = math.max(0, settings.concurrent_title_resolve_limit - ongoing_url_requests_count)
+ for index = 1, amount_to_fetch, 1 do
+ local file = url_titles_to_fetch.pop()
+ if file then
+ ongoing_url_requests[file] = true
+ resolve_ytdl_title(file)
+ end
+ end
+end
+
+url_title_fetch_timer = mp.add_periodic_timer(0.1, url_fetching_throttler)
+url_title_fetch_timer:kill()
+
+local_request_queue = {}
+function local_request_queue.push(item)
+ table.insert(local_request_queue, item)
+end
+function local_request_queue.pop()
+ return table.remove(local_request_queue, 1)
+end
+local local_titles_to_fetch = local_request_queue
+local ongoing_local_request = false
+
+-- this will only allow 1 concurrent local title resolve process
+function local_fetching_throttler()
+ if not ongoing_local_request then
+ local file = local_titles_to_fetch.pop()
+ if file then
+ ongoing_local_request = true
+ resolve_ffprobe_title(file)
+ end
+ end
+end
+
+function resolve_titles()
+ if settings.prefer_titles == "none" then
+ return
+ end
+ if not settings.resolve_url_titles and not settings.resolve_local_titles then
+ return
+ end
+
+ local length = mp.get_property_number("playlist-count", 0)
+ if length < 2 then
+ return
+ end
+ -- loop all items in playlist because we can't predict how it has changed
+ local added_urls = false
+ local added_local = false
+ for i = 0, length - 1, 1 do
+ local filename = mp.get_property("playlist/" .. i .. "/filename")
+ local title = mp.get_property("playlist/" .. i .. "/title")
+ if i ~= pos and filename and not title and not title_table[filename] and not requested_titles[filename] then
+ requested_titles[filename] = true
+ if filename:match("^https?://") then
+ url_titles_to_fetch.push(filename)
+ added_urls = true
+ elseif settings.prefer_titles == "all" then
+ local_titles_to_fetch.push(filename)
+ added_local = true
+ end
+ end
+ end
+ if added_urls then
+ url_title_fetch_timer:resume()
+ end
+ if added_local then
+ local_fetching_throttler()
+ end
+end
+
+function resolve_ytdl_title(filename)
+ local args = {
+ settings.youtube_dl_executable,
+ "--no-playlist",
+ "--flat-playlist",
+ "-sJ",
+ "--no-config",
+ filename,
+ }
+ local req = mp.command_native_async({
+ name = "subprocess",
+ args = args,
+ playback_only = false,
+ capture_stdout = true,
+ }, function(success, res)
+ ongoing_url_requests[filename] = false
+ if res.killed_by_us then
+ msg.verbose("Request to resolve url title " .. filename .. " timed out")
+ return
+ end
+ if res.status == 0 then
+ local json, err = utils.parse_json(res.stdout)
+ if not err then
+ local is_playlist = json["_type"] and json["_type"] == "playlist"
+ local title = (is_playlist and "[playlist]: " or "") .. json["title"]
+ msg.verbose(filename .. " resolved to '" .. title .. "'")
+ title_table[filename] = title
+ refresh_UI()
+ else
+ msg.error("Failed parsing json, reason: " .. (err or "unknown"))
+ end
+ else
+ msg.error("Failed to resolve url title " .. filename .. " Error: " .. (res.error or "unknown"))
+ end
+ end)
+
+ mp.add_timeout(settings.resolve_title_timeout, function()
+ mp.abort_async_command(req)
+ ongoing_url_requests[filename] = false
+ end)
+end
+
+function resolve_ffprobe_title(filename)
+ local args = { "ffprobe", "-show_format", "-show_entries", "format=tags", "-loglevel", "quiet", filename }
+ local req = mp.command_native_async({
+ name = "subprocess",
+ args = args,
+ playback_only = false,
+ capture_stdout = true,
+ }, function(success, res)
+ ongoing_local_request = false
+ local_fetching_throttler()
+ if res.killed_by_us then
+ msg.verbose("Request to resolve local title " .. filename .. " timed out")
+ return
+ end
+ if res.status == 0 then
+ local title = string.match(res.stdout, "title=([^\n\r]+)")
+ if title then
+ msg.verbose(filename .. " resolved to '" .. title .. "'")
+ title_table[filename] = title
+ refresh_UI()
+ end
+ else
+ msg.error("Failed to resolve local title " .. filename .. " Error: " .. (res.error or "unknown"))
+ end
+ end)
+end
+
+--script message handler
+function handlemessage(msg, value, value2)
+ if msg == "show" and value == "playlist" then
+ if value2 ~= "toggle" then
+ showplaylist(value2)
+ return
+ else
+ toggle_playlist(showplaylist)
+ return
+ end
+ end
+ if msg == "show" and value == "playlist-nokeys" then
+ if value2 ~= "toggle" then
+ showplaylist_non_interactive(value2)
+ return
+ else
+ toggle_playlist(showplaylist_non_interactive)
+ return
+ end
+ end
+ if msg == "show" and value == "filename" and strippedname and value2 then
+ mp.commandv("show-text", strippedname, tonumber(value2) * 1000)
+ return
+ end
+ if msg == "show" and value == "filename" and strippedname then
+ mp.commandv("show-text", strippedname)
+ return
+ end
+ if msg == "sort" then
+ sortplaylist(value)
+ return
+ end
+ if msg == "shuffle" then
+ shuffleplaylist()
+ return
+ end
+ if msg == "reverse" then
+ reverseplaylist()
+ return
+ end
+ if msg == "loadfiles" then
+ playlist(value)
+ return
+ end
+ if msg == "save" then
+ save_playlist(value)
+ return
+ end
+ if msg == "playlist-next" then
+ playlist_next()
+ return
+ end
+ if msg == "playlist-prev" then
+ playlist_prev()
+ return
+ end
+ if msg == "playlist-next-random" then
+ playlist_random()
+ return
+ end
+ if msg == "enable-interactive-save" then
+ interactive_save = true
+ end
+ if msg == "close" then
+ remove_keybinds()
+ end
+end
+
+mp.register_script_message("playlistmanager", handlemessage)
+
+bind_keys(settings.key_sortplaylist, "sortplaylist", function()
+ sortplaylist()
+ sort_mode = sort_mode + 1
+ if sort_mode > #sort_modes then
+ sort_mode = 1
+ end
+end)
+bind_keys(settings.key_shuffleplaylist, "shuffleplaylist", shuffleplaylist)
+bind_keys(settings.key_reverseplaylist, "reverseplaylist", reverseplaylist)
+bind_keys(settings.key_loadfiles, "loadfiles", playlist)
+bind_keys(settings.key_saveplaylist, "saveplaylist", activate_playlist_save)
+bind_keys(settings.key_showplaylist, "showplaylist", showplaylist)
+bind_keys(settings.key_peek_at_playlist, "peek_at_playlist", handle_complex_playlist_toggle, { complex = true })
+
+mp.register_event("start-file", on_start_file)
+mp.register_event("file-loaded", on_file_loaded)
+mp.register_event("end-file", on_end_file)