
-- https://github.com/stax76/mpv-scripts

----- options

local o = {
    font_size = 16,
    scale_by_window = false,
    lines_to_show = 12,
    pause_on_open = false, -- does not work on my system when enabled, menu won't show
    resume_on_exit = "only-if-was-paused",

    -- styles
    line_bottom_margin = 1,
    menu_x_padding = 5,
    menu_y_padding = 2,

    use_mediainfo = false, -- # true requires the MediaInfo CLI app being installed
    stream_quality_options = "2160,1440,1080,720,480",
    aspect_ratios = "4:3,16:9,2.35:1,1.36,1.82,0,-1",
}

local opt = require "mp.options"
opt.read_options(o)

----- string

function is_empty(input)
    if input == nil or input == "" then
        return true
    end
end

function contains(input, find)
    if not is_empty(input) and not is_empty(find) then
        return input:find(find, 1, true)
    end
end

function starts_with(str, start)
    return str:sub(1, #start) == start
end

function split(input, sep)
    assert(#sep == 1) -- supports only single character separator
    local tbl = {}

    if input ~= nil then
        for str in string.gmatch(input, "([^" .. sep .. "]+)") do
            table.insert(tbl, str)
        end
    end

    return tbl
end

function replace(str, what, with)
    what = string.gsub(what, "[%(%)%.%+%-%*%?%[%]%^%$%%]", "%%%1")
    with = string.gsub(with, "[%%]", "%%%%")
    return string.gsub(str, what, with)
end

function first_to_upper(str)
    return (str:gsub("^%l", string.upper))
end

----- list

function list_contains(list, value)
    for _, v in pairs(list) do
        if v == value then
            return true
        end
    end
end

----- path

function get_temp_dir()
    local is_windows = package.config:sub(1,1) == "\\"

    if is_windows then
        return os.getenv("TEMP") .. "\\"
    else
        return "/tmp/"
    end
end

---- file

function file_exists(path)
    if is_empty(path) then return false end
    local file = io.open(path, "r")

    if file ~= nil then
        io.close(file)
        return true
    end
end

function file_write(path, content)
    local file = assert(io.open(path, "w"))
    file:write(content)
    file:close()
end

----- mpv

local utils = require "mp.utils"
local assdraw = require 'mp.assdraw'
local msg = require "mp.msg"

----- path mpv

function file_name(value)
    local _, filename = utils.split_path(value)
    return filename
end

----- main

local command_palette_version = 2
local BluRayTitles = {}
local dpiScale = 0
local originalFontSize = o.font_size

mp.commandv('script-message', 'command-palette-version', command_palette_version)

function get_mpv_version()
    local version = mp.get_property("mpv-version")
    local major, minor = version:match("^mpv v(%d+)%.(%d+)%.")

    if major and minor then
        return tonumber(major), tonumber(minor)
    end
end

local major, minor = get_mpv_version()

if major == nil or minor == nil then
    msg.error("Failed getting mpv version.")
end

if major == nil or minor == nil or (major > 0 or (major == 0 and minor > 35)) then
    mp.set_property_native("user-data/command-palette/version", command_palette_version)
end

mp.enable_messages("info")

mp.register_event('log-message', function(e)
    if e.prefix ~= "bd" then
        return
    end

    if contains(e.text, " 0 duration: ") then
        BluRayTitles = {}
    end

    if contains(e.text, " duration: ") then
        local match = string.match(e.text, "%d%d:%d%d:%d%d")

        if match then
            table.insert(BluRayTitles, match)
        end
    end
end)

local uosc_available = false
package.path = mp.command_native({ "expand-path", "~~/script-modules/?.lua;" }) .. package.path

local em = require "extended-menu"
local menu = em:new(o)
local menu_content = { list = {}, current_i = nil }
local media_info_cache = {}
local original_set_active_func = em.set_active
local original_get_line_func = em.get_line

function em:get_bindings()
    local bindings = {
        { 'esc',         function() self:set_active(false) end                         },
        { 'enter',       function() self:handle_enter() end                            },
        { 'bs',          function() self:handle_backspace() end                        },
        { 'del',         function() self:handle_del() end                              },
        { 'ins',         function() self:handle_ins() end                              },
        { 'left',        function() self:prev_char() end                               },
        { 'right',       function() self:next_char() end                               },
        { 'ctrl+f',      function() self:next_char() end                               },
        { 'up',          function() self:change_selected_index(-1) end                 },
        { 'down',        function() self:change_selected_index(1) end                  },
        { 'ctrl+up',     function() self:move_history(-1) end                          },
        { 'ctrl+down',   function() self:move_history(1) end                           },
        { 'ctrl+left',   function() self:prev_word() end                               },
        { 'ctrl+right',  function() self:next_word() end                               },
        { 'home',        function() self:go_home() end                                 },
        { 'end',         function() self:go_end() end                                  },
        { 'pgup',        function() self:change_selected_index(-o.lines_to_show) end   },
        { 'pgdwn',       function() self:change_selected_index(o.lines_to_show) end    },
        { 'ctrl+u',      function() self:del_to_start() end                            },
        { 'ctrl+v',      function() self:paste(true) end                               },
        { 'ctrl+bs',     function() self:del_word() end                                },
        { 'ctrl+del',    function() self:del_next_word() end                           },
        { 'kp_dec',      function() self:handle_char_input('.') end                    },
        { 'mbtn_left',   function() self:handle_enter() end                            },
        { 'mbtn_right',  function() self:set_active(false) end                         },
        { 'wheel_up',    function() self:change_selected_index(-1) end                 },
        { 'wheel_down',  function() self:change_selected_index(1) end                  },
        { 'mbtn_forward',function() self:change_selected_index(-o.lines_to_show) end   },
        { 'mbtn_back',   function() self:change_selected_index(o.lines_to_show) end    },
    }

    for i = 0, 9 do
        bindings[#bindings + 1] = {'kp' .. i, function() self:handle_char_input('' .. i) end}
    end

    return bindings
end

function em:set_active(active)
    original_set_active_func(self, active)

    if not active then
        if osc_visibility == "auto" or osc_visibility == "always" then
            mp.command("script-message osc-visibility " .. osc_visibility .. " no_osd")
            osc_visibility = nil
        elseif uosc_available then
            mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(), '')
        end
    end
end

menu.index_field = "index"

local function format_time(t, duration)
    local h = math.floor(t / (60 * 60))
    t = t - (h * 60 * 60)
    local m = math.floor(t / 60)
    local s = t - (m * 60)

    if duration >= 60 * 60 or h > 0 then
        return string.format("%.2d:%.2d:%.2d", h, m, s)
    end

    return string.format("%.2d:%.2d", m, s)
end

function get_media_info()
    local path = mp.get_property("path")

    if contains(path, "://") or not file_exists(path) then
        return
    end

    if media_info_cache[path] then
        return media_info_cache[path]
    end

    local format_file = get_temp_dir() .. mp.get_script_name() .. " media-info-format-v1.txt"

    if not file_exists(format_file) then
        media_info_format = [[General;N: %FileNameExtension%\\nG: %Format%, %FileSize/String%, %Duration/String%, %OverallBitRate/String%, %Recorded_Date%\\n
Video;V: %Format%, %Format_Profile%, %Width%x%Height%, %BitRate/String%, %FrameRate% FPS\\n
Audio;A: %Language/String%, %Format%, %Format_Profile%, %BitRate/String%, %Channel(s)% ch, %SamplingRate/String%, %Title%\\n
Text;S: %Language/String%, %Format%, %Format_Profile%, %Title%\\n]]

        file_write(format_file, media_info_format)
    end

    local proc_result = mp.command_native({
        name = "subprocess",
        playback_only = false,
        capture_stdout = true,
        args = {"mediainfo", "--inform=file://" .. format_file, path},
    })

    if proc_result.status == 0 then
        local output = proc_result.stdout

        output = string.gsub(output, ", , ,", ",")
        output = string.gsub(output, ", ,", ",")
        output = string.gsub(output, ": , ", ": ")
        output = string.gsub(output, ", \\n\r*\n", "\\n")
        output = string.gsub(output, "\\n\r*\n", "\\n")
        output = string.gsub(output, ", \\n", "\\n")
        output = string.gsub(output, "\\n", "\n")
        output = string.gsub(output, "%.000 FPS", " FPS")
        output = string.gsub(output, "MPEG Audio, Layer 3", "MP3")

        media_info_cache[path] = output

        return output
    end
end

function binding_get_line(self, _, v)
    local ass = assdraw.ass_new()
    local cmd = self:ass_escape(v.cmd)
    local key = self:ass_escape(v.key)
    local comment = self:ass_escape(v.comment or '')

    if v.priority == -1 or v.priority == -2 then
        local why_inactive = (v.priority == -1) and 'Inactive' or 'Shadowed'
        ass:append(self:get_font_color('comment'))

        if comment ~= "" then
            ass:append(comment .. '\\h')
        end

        ass:append(key .. '\\h(' .. why_inactive .. ')' .. '\\h' .. cmd)
        return ass.text
    end

    if comment ~= "" then
        ass:append(self:get_font_color('default'))
        ass:append(comment .. '\\h')
    end

    ass:append(self:get_font_color('accent'))
    ass:append(key)
    ass:append(self:get_font_color('comment'))
    ass:append(' ' .. cmd)
    return ass.text
end

function command_palette_get_line(self, _, v)
    local ass = assdraw.ass_new()

    if v.key == "" then
        ass:append(self:get_font_color('default'))
        ass:append(self:ass_escape(v.name or ''))
    else
        ass:append(self:get_font_color('default'))
        ass:append(self:ass_escape(v.name or '') .. '\\h')

        ass:append(self:get_font_color('accent'))
        ass:append(self:ass_escape("(" .. v.key .. ")"))
    end

    return ass.text
end

local function format_flags(track)
    local flags = ""

    for _, flag in ipairs({"default", "forced", "dependent", "visual-impaired",
                           "hearing-impaired", "image", "external"}) do
        if track[flag] then
            flags = flags .. flag .. " "
        end
    end

    if flags == "" then
        return ""
    end

    return " [" .. flags:sub(1, -2) .. "]"
end

local function fix_codec(value)
    if contains(value, "hdmv_pgs_subtitle") then
        value = replace(value, "hdmv_pgs_subtitle", "pgs")
    end

    return value:upper()
end

local function get_language(lng)
    if lng == nil or lng == "" then
        return lng
    end

    if lng == "ara" then return "Arabic" end
    if lng == "ben" then return "Bangla" end
    if lng == "bng" then return "Bangla" end
    if lng == "chi" then return "Chinese" end
    if lng == "zho" then return "Chinese" end
    if lng == "eng" then return "English" end
    if lng == "fre" then return "French" end
    if lng == "fra" then return "French" end
    if lng == "ger" then return "German" end
    if lng == "deu" then return "German" end
    if lng == "hin" then return "Hindi" end
    if lng == "ita" then return "Italian" end
    if lng == "jpn" then return "Japanese" end
    if lng == "kor" then return "Korean" end
    if lng == "msa" then return "Malay" end
    if lng == "por" then return "Portuguese" end
    if lng == "pan" then return "Punjabi" end
    if lng == "rus" then return "Russian" end
    if lng == "spa" then return "Spanish" end
    if lng == "und" then return "Undetermined" end

    return lng
end

local function format_track(track)
    local lng = get_language(track.lang)
    return (track.selected and "●" or "○") .. (
            (lng and lng .. " " or "") ..
            fix_codec(track.codec and track.codec .. " " or "") ..
            (track["demux-w"] and track["demux-w"] .. "x" .. track["demux-h"]
             .. " " or "") ..
            (track["demux-fps"] and not track.image
             and string.format("%.4f", track["demux-fps"]):gsub("%.?0*$", "") ..
             " fps " or "") ..
            (track["demux-channel-count"] and track["demux-channel-count"] ..
             "ch " or "") ..
            (track["codec-profile"] and track.type == "audio"
             and track["codec-profile"] .. " " or "") ..
            (track["demux-samplerate"] and track["demux-samplerate"] / 1000 ..
             " kHz " or "") ..
            (track["demux-bitrate"] and string.format("%.0f", track["demux-bitrate"] / 1000)
             .. " kbps " or "") ..
            (track["hls-bitrate"] and string.format("%.0f", track["hls-bitrate"] / 1000)
             .. " HLS kbps " or "")
        ):sub(1, -2) .. format_flags(track) .. (track.title and " " .. track.title or "")
end

local function select(conf)
    for k, v in ipairs(conf.items) do
        table.insert(menu_content.list, { index = k, content = v })
    end

    if conf.default_item then
        menu_content.current_i = conf.default_item
    end

    function menu:submit(value)
        conf.submit(value)
    end
end

local function select_track(property, type, error)
    local tracks = {}
    local items = {}
    local default_item
    local track_id = mp.get_property_native(property)

    for _, track in ipairs(mp.get_property_native("track-list")) do
        if track.type == type then
            tracks[#tracks + 1] = track
            items[#items + 1] = format_track(track)

            if track.id == track_id then
                default_item = #items
            end
        end
    end

    if #items == 0 then
        mp.commandv("show-text", error)
        return
    end

    select({
        items = items,
        default_item = default_item,
        submit = function (tbl)
            mp.command("set " .. property .. " " ..
                (tracks[tbl.index].selected and "no" or tracks[tbl.index].id))
        end,
    })
end

function hide_osc()
    if is_empty(mp.get_property("path")) and not is_older_than_v0_36 then
        osc_visibility = mp.get_property_native("user-data/osc/visibility")

        if osc_visibility == "auto" or osc_visibility == "always" then
            mp.command("script-message osc-visibility never no_osd")
        end
    end

    if uosc_available then
        local disable_elements = "window_border, top_bar, timeline, controls, volume, idle_indicator, audio_indicator, buffering_indicator, pause_indicator"
        mp.commandv('script-message-to', 'uosc', 'disable-elements', mp.get_script_name(), disable_elements)
    end
end

mp.register_script_message("show-command-palette", function (name)
    if dpiScale == 0 then
        dpiScale = mp.get_property_native("display-hidpi-scale", 1)
    end

    o.font_size = originalFontSize * dpiScale

    menu_content.list = {}
    menu_content.current_i = 1
    menu.search_heading = name
    menu.filter_by_fields = { "content" }
    em.get_line = original_get_line_func

    if name == "Command Palette" then
        local menu_items = {}
        local bindings = utils.parse_json(mp.get_property("input-bindings"))

        local items = {
            "Playlist",
            "Tracks",
            "Video Tracks",
            "Audio Tracks",
            "Subtitle Tracks",
            "Secondary Subtitle",
            "Subtitle Line",
            "Chapters",
            "Profiles",
            "Bindings",
            "Commands",
            "Properties",
            "Options",
            "Audio Devices",
            "Blu-ray Titles",
            "Stream Quality",
            "Aspect Ratio",
            "Command Palette",
            "Recent Files",
        }

        for _, item in ipairs(items) do
            local found = false

            for _, binding in ipairs(bindings) do
                if contains(binding.cmd, "show-command-palette") and
                    (contains(binding.cmd, '"' .. item .. '"') or
                    contains(binding.cmd, "'" .. item .. "'")) then

                    table.insert(menu_items, { name = item, key = binding.key, cmd = binding.cmd })
                    found = true
                    break
                end
            end

            if not found then
                local cmd = "script-message-to command_palette show-command-palette '" .. item .. "'"
                table.insert(menu_items, { name = item, key = "", cmd = cmd })
            end
        end

        menu_content.list = menu_items

        function menu:submit(tbl)
            mp.command(tbl.cmd)
        end

        menu.filter_by_fields = {'name', 'key'}
        em.get_line = command_palette_get_line
    elseif name == "Bindings" then
        local bindings = utils.parse_json(mp.get_property("input-bindings"))

        for _, v in ipairs(bindings) do
            v.key = "(" .. v.key .. ")"

            if not is_empty(v.comment) then
                if contains(v.comment, "custom-menu: ") then
                    v.comment = replace(v.comment, "custom-menu: ", "")
                end

                if contains(v.comment, "menu: ") then
                    v.comment = replace(v.comment, "menu: ", "")
                end

                v.comment = first_to_upper(v.comment)
            end
        end

        for _, v in ipairs(bindings) do
            for _, v2 in ipairs(bindings) do
                if v.key == v2.key and v.priority < v2.priority then
                    v.priority = -2
                    break
                end
            end
        end

        table.sort(bindings, function(i, j)
            return i.priority > j.priority
        end)

        menu_content.list = bindings

        function menu:submit(tbl)
            mp.command(tbl.cmd)
        end

        menu.filter_by_fields = {'cmd', 'key', 'comment'}
        em.get_line = binding_get_line
    elseif name == "Chapters" then
        local default_index = mp.get_property_native("chapter")

        if not default_index then
            mp.commandv("show-text", "Chapter: (unavailable)")
            return
        end

        local duration = mp.get_property_native("duration", math.huge)

        for i, chapter in ipairs(mp.get_property_native("chapter-list")) do
            table.insert(menu_content.list, { index = i, content = format_time(chapter.time, duration) .. " " .. chapter.title })
        end

        menu_content.current_i = default_index + 1

        function menu:submit(tbl)
            mp.set_property_number("chapter", tbl.index - 1)
        end
    elseif name == "Playlist" then
        local count = mp.get_property_number("playlist-count")
        if count == 0 then return end

        for i = 0, (count - 1) do
            local text = mp.get_property("playlist/" .. i .. "/title")

            if text == nil then
                text = file_name(mp.get_property("playlist/" .. i .. "/filename"))
            end

            table.insert(menu_content.list, { index = i + 1, content = text })
        end

        menu_content.current_i = mp.get_property_number("playlist-pos") + 1

        function menu:submit(tbl)
            mp.set_property_number("playlist-pos", tbl.index - 1)
        end
    elseif name == "Commands" then
        local commands = utils.parse_json(mp.get_property("command-list"))

        for k, v in ipairs(commands) do
            local text = v.name

            for _, arg in ipairs(v.args) do
                if arg.optional then
                    text = text .. " [<" .. arg.name .. ">]"
                else
                    text = text .. " <" .. arg.name .. ">"
                end
            end

            table.insert(menu_content.list, { index = k, content = text })
        end

        function menu:submit(tbl)
            print(tbl.content)
            local cmd = string.match(tbl.content, '%S+')
            mp.commandv("script-message-to", "console", "type", cmd .. " ")
        end
    elseif name == "Properties" then
        local properties = split(mp.get_property("property-list"), ",")

        for k, v in ipairs(properties) do
            table.insert(menu_content.list, { index = k, content = v })
        end

        function menu:submit(tbl)
            mp.commandv('script-message-to', 'console', 'type', "print-text ${" .. tbl.content .. "}")
        end
    elseif name == "Options" then
        local options = split(mp.get_property("options"), ",")

        for k, v in ipairs(options) do
            local type = mp.get_property_osd("option-info/" .. v .. "/type", "")
            local default =mp.get_property_osd("option-info/" .. v .. "/default-value", "")
            v = v .. "   (type: " .. type .. ", default: " .. default .. ")"
            table.insert(menu_content.list, { index = k, content = v })
        end

        function menu:submit(tbl)
            print(tbl.content)
            local prop = string.match(tbl.content, '%S+')
            mp.commandv("script-message-to", "console", "type", "set " .. prop .. " ")
        end
    elseif name == "Profiles" then
        local profiles = utils.parse_json(mp.get_property("profile-list"))
        local ignore_list = {"builtin-pseudo-gui", "encoding", "libmpv", "pseudo-gui", "default"}

        for k, v in ipairs(profiles) do
            if not list_contains(ignore_list, v.name) then
                table.insert(menu_content.list, { index = k, content = v.name })
            end
        end

        function menu:submit(tbl)
            mp.command("show-text " .. tbl.content);
            mp.command("apply-profile " .. tbl.content);
        end
    elseif name == "Audio Devices" then
        local devices = utils.parse_json(mp.get_property("audio-device-list"))
        local current_name = mp.get_property("audio-device")

        for k, v in ipairs(devices) do
            table.insert(menu_content.list, { index = k, name = v.name, content = v.description })

            if v.name == current_name then
                menu_content.current_i = k
            end
        end

        function menu:submit(tbl)
            mp.commandv("set", "audio-device", tbl.name)
            mp.commandv("show-text", "audio-device: " .. tbl.content)
        end
    elseif name == "Aspect Ratio" then
        local current_ar = mp.get_property_number("video-aspect-override")

        for k, v in ipairs(split(o.aspect_ratios, ",")) do
            local display_name = v

            if display_name == "0"  then display_name = "0 (square pixels)" end
            if display_name == "-1" then display_name = "-1 (original)" end

            table.insert(menu_content.list, { index = k, content = display_name, value = v })

            local w, h = string.match(v, "^([0-9.]+):([0-9.]+)$")

            if w and h then
                local current_ar_truncated = tonumber(string.format("%.3f", current_ar))
                local ar_truncated = tonumber(string.format("%.3f", w / h))

                if current_ar_truncated == ar_truncated then
                    menu_content.current_i = k
                end
            elseif v == tostring(current_ar) then
                menu_content.current_i = k
            end
        end

        function menu:submit(tbl)
            mp.command("set video-aspect-override " .. tbl.value)
        end
    elseif name == "Stream Quality" then
        local ytdl_format = mp.get_property_native('ytdl-format')

        for k, v in ipairs(split(o.stream_quality_options, ",")) do
            local format = 'bestvideo[height<=?' .. v .. ']+bestaudio/best[height<=?' .. v .. ']'
            table.insert(menu_content.list, { index = k, content = v .. 'p', value = format })

            if format == ytdl_format then
                menu_content.current_i = k
            end
        end

        function menu:submit(tbl)
            mp.set_property('ytdl-format', tbl.value)
            mp.commandv("show-text", "Stream Quality: " .. tbl.content)

            local duration = mp.get_property_native('duration')
            local time_pos = mp.get_property('time-pos')

            mp.command('playlist-play-index current')

            if duration and duration > 0 then
                local function seeker()
                    mp.commandv('seek', time_pos, 'absolute')
                    mp.unregister_event(seeker)
                end

                mp.register_event('file-loaded', seeker)
            end
        end
    elseif name == "Tracks" then
        local tracks = {}

        for i, track in ipairs(mp.get_property_native("track-list")) do
            local type = track.image and "I" or track.type

            if type == "video" then type = "V" end
            if type == "audio" then type = "A" end
            if type == "sub" then type = "S" end

            tracks[i] = type .. ": " .. format_track(track)
        end

        if #tracks == 0 then
            mp.commandv("show-text", "No available tracks")
            return
        end

        select({
            items = tracks,
            submit = function (tbl)
                local track = mp.get_property_native("track-list/" .. tbl.index - 1)

                if track then
                    mp.command("set " .. track.type .. " " .. (track.selected and "no" or track.id))
                end
            end,
        })
    elseif name == "Audio Tracks" then
        if o.use_mediainfo then
            local mi = get_media_info()
            if mi == nil then return end
            local tracks = split(mi .. "\nA: None", "\n")
            local id = 0

            for _, v in ipairs(tracks) do
                if starts_with(v, "A: ") then
                    id = id + 1
                    table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
                end
            end

            menu_content.current_i = mp.get_property_number("aid") or id

            function menu:submit(tbl)
                mp.command("set aid " .. ((tbl.index == id) and 'no' or tbl.index))
            end
        else
            select_track("aid", "audio", "No available audio tracks")
        end
    elseif name == "Subtitle Tracks" then
        if o.use_mediainfo then
            local mi = get_media_info()
            if mi == nil then return end
            local tracks = split(mi .. "\nS: None", "\n")
            local id = 0

            for _, v in ipairs(tracks) do
                if starts_with(v, "S: ") then
                    id = id + 1
                    table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
                end
            end

            menu_content.current_i = mp.get_property_number("sid") or id

            function menu:submit(tbl)
                mp.command("set sid " .. ((tbl.index == id) and 'no' or tbl.index))
            end
        else
            select_track("sid", "sub", "No available subtitle tracks")
        end
    elseif name == "Secondary Subtitle" then
        select_track("secondary-sid", "sub", "No available subtitle tracks")
    elseif name == "Recent Files" then
        local frontend = mp.get_property_native("user-data/frontend/name")

        if frontend == "mpv.net" then
            mp.command("script-message show-recent-in-command-palette")
        else
            mp.command("script-message open-recent-menu command-palette")
        end

        return
    elseif name == "Video Tracks" then
        if o.use_mediainfo then
            local mi = get_media_info()
            if mi == nil then return end
            local tracks = split(mi .. "\nV: None", "\n")
            local id = 0

            for _, v in ipairs(tracks) do
                if starts_with(v, "V: ") then
                    id = id + 1
                    table.insert(menu_content.list, { index = id, content = string.sub(v, 4) })
                end
            end

            menu_content.current_i = mp.get_property_number("vid") or id

            function menu:submit(tbl)
                mp.command("set vid " .. ((tbl.index == id) and 'no' or tbl.index))
            end
        else
            select_track("vid", "video", "No available video tracks")
        end
    elseif name == "Blu-ray Titles" then
        if #BluRayTitles == 0 then
            return
        end

        local items = {}

        for k, v in ipairs(BluRayTitles) do
            table.insert(items, "Title " .. k .. "   " .. v)
        end

        select({
            items = items,
            submit = function (tbl)
                mp.commandv("loadfile", "bd://" .. (tbl.index - 1))
            end,
        })
    elseif name == "Subtitle Line" then
        local sub = mp.get_property_native("current-tracks/sub")

        if sub == nil then
            mp.commandv("show-text", "No subtitle is loaded")
            return
        end

        if sub.external and sub["external-filename"]:find("^edl://") then
            sub["external-filename"] = sub["external-filename"]:match('https?://.*')
                                       or sub["external-filename"]
        end

        local r = mp.command_native({
            name = "subprocess",
            capture_stdout = true,
            args = sub.external
                and {"ffmpeg", "-loglevel", "error", "-i", sub["external-filename"],
                     "-f", "lrc", "-map_metadata", "-1", "-fflags", "+bitexact", "-"}
                or {"ffmpeg", "-loglevel", "error", "-i", mp.get_property("path"),
                    "-map", "s:" .. sub["id"] - 1, "-f", "lrc", "-map_metadata",
                    "-1", "-fflags", "+bitexact", "-"}
        })

        if r.error_string == "init" then
            mp.commandv("show-text", "Failed to extract subtitles: ffmpeg not found")
            return
        elseif r.status ~= 0 then
            mp.commandv("show-text", "Failed to extract subtitles")
            return
        end

        local sub_lines = {}
        local sub_times = {}
        local default_item
        local delay = mp.get_property_native("sub-delay")
        local time_pos = mp.get_property_native("time-pos") - delay
        local duration = mp.get_property_native("duration", math.huge)

        -- Strip HTML and ASS tags.
        for line in r.stdout:gsub("<.->", ""):gsub("{\\.-}", ""):gmatch("[^\n]+") do
            -- ffmpeg outputs LRCs with minutes > 60 instead of adding hours.
            sub_times[#sub_times + 1] = line:match("%d+") * 60 + line:match(":([%d%.]*)")
            sub_lines[#sub_lines + 1] = format_time(sub_times[#sub_times], duration) ..
                                        " " .. line:gsub(".*]", "", 1)

            if sub_times[#sub_times] <= time_pos then
                default_item = #sub_times
            end
        end

        select({
            items = sub_lines,
            default_item = default_item,
            submit = function (tbl)
                -- Add an offset to seek to the correct line while paused without a video track.
                if mp.get_property_native("current-tracks/video/image") ~= false then
                    delay = delay + 0.1
                end

                mp.commandv("seek", sub_times[tbl.index] + delay, "absolute")
            end,
        })
    else
        if name == nil then
            msg.error("Unknown mode")
        else
            msg.error("Unknown mode: " .. name)
        end

        return
    end

    hide_osc()
    menu:init(menu_content)
end)

mp.register_script_message('uosc-version', function(version)
    local major, minor = version:match('^(%d+)%.(%d+)')
    if major and minor and tonumber(major) >= 5 and tonumber(minor) >= 0 then
        uosc_available = true
    end
end)

mp.register_script_message("show-command-palette-json", function (json)
    if dpiScale == 0 then
        dpiScale = mp.get_property_native("display-hidpi-scale", 1)
    end

    o.font_size = originalFontSize * dpiScale

    local menu_data = utils.parse_json(json)
    menu_content.list = {}
    menu_content.current_i = 1
    menu.search_heading = menu_data.title
    menu.filter_by_fields = { "content", "hint", "value_hint" }
    em.get_line = original_get_line_func

    for k, v in ipairs(menu_data.items) do
        local values = v.value

        if type(values) == "string" then
            values = { values }
        end

        table.insert(menu_content.list, {
            index = k,
            content = v.title,
            hint = v.hint,
            values = values,
            value_hint = table.concat(values, " "),
        })

        if menu_data.selected_index then
            menu_content.current_i = menu_data.selected_index
        end
    end

    function menu:submit(tbl)
        mp.command_native(tbl.values)
    end

    hide_osc()
    menu:init(menu_content)
end)
