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 key_moveup = "UP", key_movedown = "DOWN", key_selectfile = "RIGHT LEFT", key_unselectfile = "", key_playfile = "ENTER", key_removefile = "BS", key_closeplaylist = "ESC", -- 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 = "", --[=====[ START OF SAMPLE REPLACE, to use remove start and end line --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 there is 0 or 1 items in playlist, if 0 uses worḱing dir for files loadfiles_on_start = false, -- sort playlist on mpv start sortplaylist_on_start = false, -- sort playlist when files are added to playlist sortplaylist_on_file_add = false, -- use alphanumerical sort alphanumsort = true, -- "linux | windows | auto" system = "auto", -- Use ~ for home directory. Leave as empty to use mpv/playlists playlist_savepath = "", -- show playlist or filename every time a new file is loaded -- 2 shows playlist, 1 shows current file(filename strip applied) as osd text, 0 shows nothing -- instead of using this you can also call script-message playlistmanager show playlist/filename -- ex. KEY playlist-next ; script-message playlistmanager show playlist show_playlist_on_fileload = 0, -- 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, -- playlist open key will toggle visibility instead of refresh, best used with long timeout open_toggles = true, -- allow the playlist cursor to loop from end to start and vice versa loop_cursor = 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_titles = false, -- osd timeout on inactivity, with high value on this open_toggles is good to be true playlist_display_timeout = 5, -- amount of entries to show before slicing. Optimal value depends on font/video size etc. showamount = 16, -- font size scales by window, if false requires larger font and padding sizes scale_playlist_by_window = true, -- playlist ass style overrides inside curly brackets, \keyvalue is one field, extra \ for escape in lua -- example {\\fnUbuntu\\fs10\\b0\\bord1} equals: 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 style_ass_tags = "{}", -- paddings from top left corner text_padding_x = 10, text_padding_y = 30, -- 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 = "○ %name", hovered_file = "● %name", selected_file = "➔ %name", playing_file = "▷ %name", playing_hovered_file = "▶ %name", playing_selected_file = "➤ %name", -- what to show when playlist is truncated playlist_sliced_prefix = "...", playlist_sliced_suffix = "...", } 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") -- 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 -- global variables 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 url_table = {} -- table for urls that we have request to be resolved to titles local requested_urls = {} -- state for if we sort on playlist size change local sort_watching = false local filetype_lookup = {} 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_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 if playlist_visible then showplaylist() end end update_opts({ filename_replace = true, loadfiles_filetypes = true }) function on_loaded() filename = mp.get_property("filename") path = mp.get_property("path") -- if not a url then join path with working directory if not path:match("^%a%a+:%/%/") then path = utils.join_path(mp.get_property("working-directory"), path) directory = utils.split_path(path) else directory = nil end refresh_globals() if settings.sync_cursor_on_load then cursor = pos -- refresh playlist if cursor moved if playlist_visible then draw_playlist() end end local media_title = mp.get_property("media-title") if path:match("^https?://") and not url_table[path] and path ~= media_title then url_table[path] = media_title end strippedname = stripfilename(mp.get_property("media-title")) if settings.show_playlist_on_fileload == 2 then showplaylist() elseif settings.show_playlist_on_fileload == 1 then mp.commandv("show-text", strippedname) end if settings.set_title_stripped then mp.set_property("title", settings.title_prefix .. strippedname .. settings.title_suffix) end local didload = false if settings.loadfiles_on_start and plen == 1 then didload = true -- save reference for sorting msg.info("Loading files from playing files directory") playlist() end -- if we promised to sort files on launch do it if promised_sort then promised_sort = false msg.info("Your playlist is sorted before starting playback") if didload then sortplaylist() else sortplaylist(true) end end -- if we promised to listen and sort on playlist size increase do it if promised_sort_watch then promised_sort_watch = false sort_watching = true msg.info("Added files will be automatically sorted") mp.observe_property("playlist-count", "number", autosort) end end function on_closed() strippedname = nil path = nil directory = nil filename = nil if playlist_visible then showplaylist() end 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 -- 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 (v["ext"] and (v["ext"][ext] or (ext and not protocol and v["ext"]["all"]))) or (v["protocol"] and (v["protocol"][protocol] or (protocol and not ext and v["protocol"]["all"]))) then for ruleindex, indexrules in ipairs(v["rules"]) do for rule, override in pairs(indexrules) do 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 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 name:match("^https?://") 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 url_table[name] then url_table[name] = mtitle end title = mtitle elseif url_table[name] then title = url_table[name] end end -- if we have media title use a more conservative strip if title and not notitle and should_use_title then return stripfilename(title, true) 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) 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") :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() ass:new_event() ass:pos(settings.text_padding_x, settings.text_padding_y) ass:append(settings.style_ass_tags) if settings.playlist_header ~= "" then ass:append(parse_header(settings.playlist_header) .. "\\N") end local start = cursor - math.floor(settings.showamount / 2) local showall = false local showrest = false if start < 0 then start = 0 end if plen <= settings.showamount then start = 0 showall = true end if start > math.max(plen - settings.showamount - 1, 0) then start = plen - settings.showamount showrest = true end if start > 0 and not showall then ass:append(settings.playlist_sliced_prefix .. "\\N") end for index = start, start + settings.showamount - 1, 1 do if index == plen then break end ass:append(parse_filename_by_index(index) .. "\\N") if index == start + settings.showamount - 1 and not showall and not showrest then ass:append(settings.playlist_sliced_suffix) end end local w, h = mp.get_osd_size() if settings.scale_playlist_by_window then w, h = 0, 0 end mp.set_osd_ass(w, h, ass.text) end function toggle_playlist() if settings.open_toggles then if playlist_visible then remove_keybinds() return end end showplaylist() end function showplaylist(duration) refresh_globals() if plen == 0 then return end playlist_visible = true add_keybinds() draw_playlist() keybindstimer:kill() if duration then keybindstimer = mp.add_periodic_timer(duration, remove_keybinds) else keybindstimer:resume() 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 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 showplaylist() 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 Watch_later() if mp.get_property_bool("save-position-on-quit") then mp.command("write-watch-later-config") 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 mp.set_property("playlist-pos", cursor) else if cursor ~= plen - 1 then cursor = cursor + 1 end Watch_later() mp.commandv("playlist-next", "weak") end if settings.show_playlist_on_fileload ~= 2 then remove_keybinds() end end function get_files_windows(dir) local args = { "powershell", "-NoProfile", "-Command", [[& { Trap { Write-Error -ErrorRecord $_ Exit 1 } $path = "]] .. dir .. [[" $escapedPath = [WildcardPattern]::Escape($path) cd $escapedPath $list = (Get-ChildItem -File | Sort-Object { [regex]::Replace($_.Name, '\d+', { $args[0].Value.PadLeft(20) }) }).Name $string = ($list -join "/") $u8list = [System.Text.Encoding]::UTF8.GetBytes($string) [Console]::OpenStandardOutput().Write($u8list, 0, $u8list.Length) }]], } local process = utils.subprocess({ args = args, cancellable = false }) return parse_files(process, "%/") end function get_files_linux(dir) local args = { "ls", "-1pv", dir } local process = utils.subprocess({ args = args, cancellable = false }) return parse_files(process, "\n") end function parse_files(res, delimiter) if not res.error and res.status == 0 then local valid_files = {} for line in res.stdout:gmatch("[^" .. delimiter .. "]+") do local ext = line:match("^.+%.(.+)$") if ext and filetype_lookup[ext:lower()] then table.insert(valid_files, line) end end return valid_files, nil else return nil, res.error end 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 force_dir then dir = force_dir end local files, error if settings.system == "linux" then files, error = get_files_linux(dir) else files, error = get_files_windows(dir) end local c, c2 = 0, 0 if files then local cur = false local filename = mp.get_property("filename") for _, file in ipairs(files) do local appendstr = "append" if not hasfile then cur = true appendstr = "append-play" hasfile = true end if cur == true then mp.commandv("loadfile", utils.join_path(dir, file), appendstr) msg.info("Appended to playlist: " .. file) c2 = c2 + 1 elseif file ~= filename then 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 else cur = true end end 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 cursor = mp.get_property_number("playlist-pos", 1) else msg.error("Could not scan for files: " .. (error or "")) end if sort_watching then msg.info("Ignoring directory structure and using playlist sort") sortplaylist() end refresh_globals() if playlist_visible then showplaylist() 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 -- saves the current playlist into a m3u file function save_playlist() 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 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 ) local savepath = utils.join_path(savepath, datestring .. "_playlist-size_" .. length .. ".m3u") 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 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 filename:match("^%a%a+:%/%/") then fullpath = utils.join_path(pwd, filename) end local title = mp.get_property("playlist/" .. i .. "/title") if title then file:write("#EXTINF:," .. title .. "\n") end file:write(fullpath, "\n") i = i + 1 end msg.info("Playlist written to: " .. savepath) 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 function dosort(a, b) if settings.alphanumsort then return alphanumsort(a, b) else return a < b end end function sortplaylist(startover) local length = mp.get_property_number("playlist-count", 0) if length < 2 then return end -- use insertion sort on playlist to make it easy to order files with playlist-move for outer = 1, length - 1, 1 do local outerfile = get_name_from_index(outer, true) local inner = outer - 1 while inner >= 0 and dosort(outerfile, get_name_from_index(inner, true)) do inner = inner - 1 end inner = inner + 1 if outer ~= inner then mp.commandv("playlist-move", outer, inner) 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 end function autosort(name, param) if param == 0 then return end if plen < param then msg.info("Playlistmanager autosorting playlist") refresh_globals() sortplaylist() 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 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)) mp.set_property("playlist-pos", 0) refresh_globals() if playlist_visible then showplaylist() end end function bind_keys(keys, name, func, opts) if not 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 not 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(settings.key_moveup, "moveup", moveup, "repeatable") bind_keys(settings.key_movedown, "movedown", movedown, "repeatable") bind_keys(settings.key_selectfile, "selectfile", selectfile) bind_keys(settings.key_unselectfile, "unselectfile", unselectfile) bind_keys(settings.key_playfile, "playfile", playfile) bind_keys(settings.key_removefile, "removefile", removefile, "repeatable") bind_keys(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() mp.set_osd_ass(0, 0, "") playlist_visible = false if settings.dynamic_binds then unbind_keys(settings.key_moveup, "moveup") unbind_keys(settings.key_movedown, "movedown") 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_start and mp.get_property_number("playlist-count", 0) == 0 then playlist() end promised_sort_watch = false if settings.sortplaylist_on_file_add then promised_sort_watch = true end promised_sort = false if settings.sortplaylist_on_start then promised_sort = true end mp.observe_property("playlist-count", "number", function() if playlist_visible then showplaylist() end if settings.prefer_titles == "none" then return end -- resolve titles resolve_titles() end) -- resolves url titles by calling youtube-dl function resolve_titles() if not settings.resolve_titles then return end local length = mp.get_property_number("playlist-count", 0) if length < 2 then return end local i = 0 -- loop all items in playlist because we can't predict how it has changed while i < length do local filename = mp.get_property("playlist/" .. i .. "/filename") local title = mp.get_property("playlist/" .. i .. "/title") if i ~= pos and filename and filename:match("^https?://") and not title and not url_table[filename] and not requested_urls[filename] then requested_urls[filename] = true local args = { "youtube-dl", "--no-playlist", "--flat-playlist", "-sJ", filename, } local req = mp.command_native_async({ name = "subprocess", args = args, playback_only = false, capture_stdout = true, }, function(success, res) 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 .. "'") url_table[filename] = title refresh_globals() if playlist_visible then showplaylist() end return 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(5, function() mp.abort_async_command(req) end) end i = i + 1 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() 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() return end end mp.register_script_message("playlistmanager", handlemessage) mp.add_key_binding("CTRL+p", "sortplaylist", sortplaylist) mp.add_key_binding("CTRL+P", "shuffleplaylist", shuffleplaylist) mp.add_key_binding("CTRL+R", "reverseplaylist", reverseplaylist) mp.add_key_binding("P", "loadfiles", playlist) mp.add_key_binding("p", "saveplaylist", save_playlist) mp.add_key_binding("SHIFT+ENTER", "showplaylist", toggle_playlist) mp.register_event("file-loaded", on_loaded) mp.register_event("end-file", on_closed)