--[[ This file is part of mpv. mpv is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. mpv is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with mpv. If not, see . ]] local utils = require "mp.utils" local input = require "mp.input" local function show_error(message) mp.msg.error(message) if mp.get_property_native("vo-configured") then mp.osd_message(message) end end mp.add_forced_key_binding(nil, "select-playlist", function () local playlist = {} local default_item local show = mp.get_property_native("osd-playlist-entry") local trailing_slash_pattern = mp.get_property("platform") == "windows" and "[/\\]+$" or "/+$" for i, entry in ipairs(mp.get_property_native("playlist")) do playlist[i] = entry.title if not playlist[i] or show ~= "title" then playlist[i] = entry.filename if not playlist[i]:find("://") then playlist[i] = select(2, utils.split_path( playlist[i]:gsub(trailing_slash_pattern, ""))) end end if entry.title and show == "both" then playlist[i] = string.format("%s (%s)", entry.title, playlist[i]) end if entry.playing then default_item = i end end if #playlist == 0 then show_error("The playlist is empty.") return end input.select({ prompt = "Select a playlist entry:", items = playlist, default_item = default_item, submit = function (index) mp.commandv("playlist-play-index", index - 1) end, }) 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 format_track(track) local bitrate = track["demux-bitrate"] or track["hls-bitrate"] return (track.selected and "●" or "○") .. (track.title and " " .. track.title or "") .. " (" .. ( (track.lang and track.lang .. " " or "") .. (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 "") .. (bitrate and string.format("%.0f", bitrate / 1000) .. " kbps " or "") ):sub(1, -2) .. ")" .. format_flags(track) end mp.add_forced_key_binding(nil, "select-track", function () local tracks = {} for i, track in ipairs(mp.get_property_native("track-list")) do tracks[i] = (track.image and "Image" or track.type:sub(1, 1):upper() .. track.type:sub(2)) .. ": " .. format_track(track) end if #tracks == 0 then show_error("No available tracks.") return end input.select({ prompt = "Select a track:", items = tracks, submit = function (id) local track = mp.get_property_native("track-list/" .. id - 1) if track then mp.set_property(track.type, track.selected and "no" or track.id) end end, }) end) local function select_track(property, type, prompt, 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 show_error(error) return end input.select({ prompt = prompt, items = items, default_item = default_item, submit = function (id) mp.set_property(property, tracks[id].selected and "no" or tracks[id].id) end, }) end mp.add_forced_key_binding(nil, "select-sid", function () select_track("sid", "sub", "Select a subtitle:", "No available subtitles.") end) mp.add_forced_key_binding(nil, "select-secondary-sid", function () select_track("secondary-sid", "sub", "Select a secondary subtitle:", "No available subtitles.") end) mp.add_forced_key_binding(nil, "select-aid", function () select_track("aid", "audio", "Select an audio track:", "No available audio tracks.") end) mp.add_forced_key_binding(nil, "select-vid", function () select_track("vid", "video", "Select a video track:", "No available video tracks.") end) 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 mp.add_forced_key_binding(nil, "select-chapter", function () local chapters = {} local default_item = mp.get_property_native("chapter") if default_item == nil then show_error("No available chapters.") return end local duration = mp.get_property_native("duration", math.huge) for i, chapter in ipairs(mp.get_property_native("chapter-list")) do chapters[i] = format_time(chapter.time, duration) .. " " .. chapter.title end input.select({ prompt = "Select a chapter:", items = chapters, default_item = default_item + 1, submit = function (chapter) mp.set_property("chapter", chapter - 1) end, }) end) mp.add_forced_key_binding(nil, "select-subtitle-line", function () local sub = mp.get_property_native("current-tracks/sub") if sub == nil then show_error("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 show_error("Failed to extract subtitles: ffmpeg not found.") return elseif r.status ~= 0 then show_error("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 input.select({ prompt = "Select a line to seek to:", items = sub_lines, default_item = default_item, submit = function (index) -- 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[index] + delay, "absolute") end, }) end) mp.add_forced_key_binding(nil, "select-audio-device", function () local devices = mp.get_property_native("audio-device-list") local items = {} -- This is only useful if an --audio-device has been explicitly set, -- otherwise its value is just auto and there is no current-audio-device -- property. local selected_device = mp.get_property("audio-device") local default_item if #devices == 0 then show_error("No available audio devices.") return end for i, device in ipairs(devices) do items[i] = device.name .. " (" .. device.description .. ")" if device.name == selected_device then default_item = i end end input.select({ prompt = "Select an audio device:", items = items, default_item = default_item, submit = function (id) mp.set_property("audio-device", devices[id].name) end, }) end) mp.add_forced_key_binding(nil, "select-binding", function () local bindings = {} for _, binding in pairs(mp.get_property_native("input-bindings")) do if binding.priority >= 0 and ( bindings[binding.key] == nil or (bindings[binding.key].is_weak and not binding.is_weak) or (binding.is_weak == bindings[binding.key].is_weak and binding.priority > bindings[binding.key].priority) ) then bindings[binding.key] = binding end end local items = {} for _, binding in pairs(bindings) do if binding.cmd ~= "ignore" then items[#items + 1] = binding.key .. " " .. binding.cmd end end table.sort(items) input.select({ prompt = "Select a binding:", items = items, submit = function (i) mp.command(items[i]:gsub("^.- ", "")) end, }) end) local properties = {} local function add_property(property, value) value = value or mp.get_property_native(property) if type(value) == "table" and next(value) then for key, val in pairs(value) do add_property(property .. "/" .. key, val) end else properties[#properties + 1] = property .. ": " .. utils.to_string(value) end end mp.add_forced_key_binding(nil, "show-properties", function () properties = {} -- Don't log errors for renamed and removed properties. local msg_level_backup = mp.get_property("msg-level") mp.set_property("msg-level", msg_level_backup == "" and "cplayer=no" or msg_level_backup .. ",cplayer=no") for _, property in pairs(mp.get_property_native("property-list")) do add_property(property) end mp.set_property("msg-level", msg_level_backup) add_property("current-tracks/audio") add_property("current-tracks/video") add_property("current-tracks/sub") add_property("current-tracks/sub2") table.sort(properties) input.select({ prompt = "Inspect a property:", items = properties, submit = function (i) if mp.get_property_native("vo-configured") then mp.commandv("expand-properties", "show-text", (#properties[i] > 100 and "${osd-ass-cc/0}{\\fs9}${osd-ass-cc/1}" or "") .. "$>" .. properties[i], 20000) else mp.msg.info(properties[i]) end end, }) end)