select.lua: add this script

This adds script messages to select playlist entries, tracks, chapters,
subtitle lines, bindings and properties using the newly introduced
mp.input.select().

This fully closes #13964.
This commit is contained in:
Guido Cella 2024-05-12 21:57:56 +02:00 committed by Kacper Michajłow
parent 367a6b561a
commit 61f72bd512
10 changed files with 417 additions and 2 deletions

View File

@ -270,6 +270,46 @@ Alt+2 (and Command+2 on macOS)
Command + f (macOS only) Command + f (macOS only)
Toggle fullscreen (see also ``--fs``). Toggle fullscreen (see also ``--fs``).
(The following keybindings open a selector in the console that lets you choose
from a list of items by typing part of the desired item and/or by navigating
them with keybindings: ``Down`` and ``Ctrl+n`` go down, ``Up`` and ``Ctrl+p`` go
up, ``Page down`` and ``Ctrl+f`` scroll down one page, and ``Page up`` and
``Ctrl+b`` scroll up one page.)
g-p
Select a playlist entry.
g-s
Select a subtitle track.
g-S
Select a secondary subtitle track.
g-a
Select an audio track.
g-v
Select a video track.
g-t
Select a track of any type.
g-c
Select a chapter.
g-l
Select a subtitle line to seek to. This currently requires ``ffmpeg`` in
``PATH``, or in the same folder as mpv on Windows.
g-d
Select an audio device.
g-b
Select a defined input binding.
g-r
Show the values of all properties.
(The following keys are valid if you have a keyboard with multimedia keys.) (The following keys are valid if you have a keyboard with multimedia keys.)
PAUSE PAUSE

View File

@ -1023,6 +1023,10 @@ Program Behavior
`Conditional auto profiles`_ for details. ``auto`` will load the script, `Conditional auto profiles`_ for details. ``auto`` will load the script,
but immediately unload it if there are no conditional profiles. but immediately unload it if there are no conditional profiles.
``--load-select=<yes|no>``
Enable the builtin script that lets you select from lists of items (default:
yes). By default, its keybindings start with the ``g`` key.
``--player-operation-mode=<cplayer|pseudo-gui>`` ``--player-operation-mode=<cplayer|pseudo-gui>``
For enabling "pseudo GUI mode", which means that the defaults for some For enabling "pseudo GUI mode", which means that the defaults for some
options are changed. This option should not normally be used directly, but options are changed. This option should not normally be used directly, but

View File

@ -168,6 +168,18 @@
#ctrl+h cycle-values hwdec "auto-safe" "no" # toggle hardware decoding #ctrl+h cycle-values hwdec "auto-safe" "no" # toggle hardware decoding
#F8 show-text ${playlist} # show the playlist #F8 show-text ${playlist} # show the playlist
#F9 show-text ${track-list} # show the list of video, audio and sub tracks #F9 show-text ${track-list} # show the list of video, audio and sub tracks
#g ignore
#g-p script-binding select/select-playlist
#g-s script-binding select/select-sid
#g-S script-binding select/select-secondary-sid
#g-a script-binding select/select-aid
#g-v script-binding select/select-vid
#g-t script-binding select/select-track
#g-c script-binding select/select-chapter
#g-l script-binding select/select-subtitle-line
#g-d script-binding select/select-audio-device
#g-b script-binding select/select-binding
#g-r script-binding select/show-properties
# #
# Legacy bindings (may or may not be removed in the future) # Legacy bindings (may or may not be removed in the future)

View File

@ -547,6 +547,7 @@ static const m_option_t mp_opts[] = {
{"load-auto-profiles", {"load-auto-profiles",
OPT_CHOICE(lua_load_auto_profiles, {"no", 0}, {"yes", 1}, {"auto", -1}), OPT_CHOICE(lua_load_auto_profiles, {"no", 0}, {"yes", 1}, {"auto", -1}),
.flags = UPDATE_BUILTIN_SCRIPTS}, .flags = UPDATE_BUILTIN_SCRIPTS},
{"load-select", OPT_BOOL(lua_load_select), .flags = UPDATE_BUILTIN_SCRIPTS},
#endif #endif
// ------------------------- stream options -------------------- // ------------------------- stream options --------------------
@ -969,6 +970,7 @@ static const struct MPOpts mp_default_opts = {
.lua_load_stats = true, .lua_load_stats = true,
.lua_load_console = true, .lua_load_console = true,
.lua_load_auto_profiles = -1, .lua_load_auto_profiles = -1,
.lua_load_select = true,
#endif #endif
.auto_load_scripts = true, .auto_load_scripts = true,
.loop_times = 1, .loop_times = 1,

View File

@ -175,6 +175,7 @@ typedef struct MPOpts {
bool lua_load_stats; bool lua_load_stats;
bool lua_load_console; bool lua_load_console;
int lua_load_auto_profiles; int lua_load_auto_profiles;
bool lua_load_select;
bool auto_load_scripts; bool auto_load_scripts;

View File

@ -436,7 +436,7 @@ typedef struct MPContext {
struct mp_ipc_ctx *ipc_ctx; struct mp_ipc_ctx *ipc_ctx;
int64_t builtin_script_ids[5]; int64_t builtin_script_ids[6];
mp_mutex abort_lock; mp_mutex abort_lock;

View File

@ -81,6 +81,9 @@ static const char * const builtin_lua_scripts[][2] = {
}, },
{"@auto_profiles.lua", {"@auto_profiles.lua",
# include "player/lua/auto_profiles.lua.inc" # include "player/lua/auto_profiles.lua.inc"
},
{"@select.lua",
# include "player/lua/select.lua.inc"
}, },
{0} {0}
}; };

View File

@ -1,6 +1,6 @@
lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua', lua_files = ['defaults.lua', 'assdraw.lua', 'options.lua', 'osc.lua',
'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua', 'ytdl_hook.lua', 'stats.lua', 'console.lua', 'auto_profiles.lua',
'input.lua', 'fzy.lua'] 'input.lua', 'fzy.lua', 'select.lua']
foreach file: lua_files foreach file: lua_files
lua_file = custom_target(file, lua_file = custom_target(file,
input: join_paths(source_root, 'player', 'lua', file), input: join_paths(source_root, 'player', 'lua', file),

352
player/lua/select.lua Normal file
View File

@ -0,0 +1,352 @@
--[[
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 <http://www.gnu.org/licenses/>.
]]
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 = 1
for i, entry in ipairs(mp.get_property_native("playlist")) do
playlist[i] = select(2, utils.split_path(entry.filename))
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_track(track)
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("%.3f", track["demux-fps"]) .. " 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.external and "external " or "")
):sub(1, -2) .. ")"
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.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 = 1
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)
local h = math.floor(t / (60 * 60))
t = t - (h * 60 * 60)
local m = math.floor(t / 60)
local s = t - (m * 60)
return string.format("%.2d:%.2d:%.2d", h, 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
for i, chapter in ipairs(mp.get_property_native("chapter-list")) do
chapters[i] = format_time(chapter.time) .. " " .. 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
local r = mp.command_native({
name = "subprocess",
capture_stdout = true,
args = sub.external
and {"ffmpeg", "-loglevel", "quiet", "-i", sub["external-filename"],
"-f", "lrc", "-map_metadata", "-1", "-fflags", "+bitexact", "-"}
or {"ffmpeg", "-loglevel", "quiet", "-i", mp.get_property("path"),
"-map", "s:" .. sub["id"] - 1, "-f", "lrc", "-map_metadata",
"-1", "-fflags", "+bitexact", "-"}
})
if r.status < 0 then
show_error("subprocess error: " .. r.error_string)
return
end
if r.status > 0 then
show_error("ffmpeg failed with code " .. r.status)
return
end
local sub_lines = {}
local default_item = 1
local sub_start = mp.get_property_native("sub-start", 0)
local m = math.floor(sub_start / 60)
local s = sub_start - m * 60
sub_start = string.format("%.2d:%05.2f", m, s)
-- Strip HTML and ASS tags.
for line in r.stdout:gsub("<.->", ""):gsub("{\\.-}", ""):gmatch("[^\n]+") do
sub_lines[#sub_lines + 1] = line
if line:find("^%[" .. sub_start) then
default_item = #sub_lines
end
end
input.select({
prompt = "Select a line to seek to:",
items = sub_lines,
default_item = default_item,
submit = function (index)
mp.commandv("seek", sub_lines[index]:match("[%d:%.]+"), "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 = 1
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)

View File

@ -262,6 +262,7 @@ void mp_load_builtin_scripts(struct MPContext *mpctx)
load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua"); load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles, load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
"@auto_profiles.lua"); "@auto_profiles.lua");
load_builtin_script(mpctx, 5, mpctx->opts->lua_load_select, "@select.lua");
} }
bool mp_load_scripts(struct MPContext *mpctx) bool mp_load_scripts(struct MPContext *mpctx)