Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442--[[This file is part of mpv.
mpv is free software; you can redistribute it and/ormodify it under the terms of the GNU Lesser General PublicLicense as published by the Free Software Foundation; eitherversion 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 ofMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See theGNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General PublicLicense along with mpv. If not, see <http://www.gnu.org/licenses/>.]]
local utils = require "mp.utils"local input = require "mp.input"
local options = { history_date_format = "%Y-%m-%d %H:%M:%S", hide_history_duplicates = true, menu_conf_path = "~~/menu.conf", max_playlist_items = 25, populate_menu_data = true,}
require "mp.options".read_options(options, nil, function () end)
local platform = mp.get_property("platform")
local function show_warning(message) mp.msg.warn(message) if mp.get_property_native("vo-configured") then mp.osd_message(message) endend
local function show_error(message) mp.msg.error(message) if mp.get_property_native("vo-configured") then mp.osd_message(message) endend
local function to_map(t) local map = {}
for _, value in pairs(t) do map[value] = true end
return mapend
local function format_playlist_entry(entry, show) local item = entry.title
if not item or show ~= "title" then item = entry.filename
if not item:find("://") then item = select(2, utils.split_path(item)) end
if entry.title and show == "both" then item = entry.title .. " (" .. item .. ")" end end
return itemend
mp.add_key_binding(nil, "select-playlist", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open(default_item) local playlist = {} local show = mp.get_property_native("osd-playlist-entry")
for i, entry in ipairs(mp.get_property_native("playlist")) do playlist[i] = format_playlist_entry(entry, show)
if not default_item and entry.playing then default_item = i end end
if #playlist == 0 then show_warning("The playlist is empty.") return end
input.select({ prompt = "Select a playlist entry:", items = playlist, default_item = default_item, keep_open = keep_open, submit = function (index) mp.commandv("playlist-play-index", index - 1) if keep_open then open(index) end end, }) end open()end, { complex = true })
local function format_track_type(track) return (track.image and "Image" or track.type:sub(1, 1):upper() .. track.type:sub(2)) .. ": "end
local function format_flags(track) local flags = ""
for _, flag in ipairs({ "default", "forced", "dependent", "visual-impaired", "hearing-impaired", "original", "commentary", "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_key_binding(nil, "select-track", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open(default_item) local tracks = {}
for i, track in ipairs(mp.get_property_native("track-list")) do tracks[i] = format_track_type(track) .. format_track(track) end
if #tracks == 0 then show_warning("No available tracks.") return end
input.select({ prompt = "Select a track:", items = tracks, default_item = default_item, keep_open = keep_open, 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 if keep_open then open(id) end end, }) end open()end, { complex = true })
local function select_track(property, type, prompt, warning, keep_open) local function open(default_item) local tracks = {} local items = {} 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 not default_item and track.id == track_id then default_item = #items end end end
if #items == 0 then show_warning(warning) return end
input.select({ prompt = prompt, items = items, default_item = default_item, keep_open = keep_open, submit = function (id) mp.set_property(property, tracks[id].selected and "no" or tracks[id].id) if keep_open then open(id) end end, }) end open()end
mp.add_key_binding(nil, "select-sid", function (t) if t.event == "up" or t.event == "repeat" then return end select_track("sid", "sub", "Select a subtitle:", "No available subtitles.", t.arg == "keep-open")end, { complex = true })
mp.add_key_binding(nil, "select-secondary-sid", function (t) if t.event == "up" or t.event == "repeat" then return end select_track("secondary-sid", "sub", "Select a secondary subtitle:", "No available subtitles.", t.arg == "keep-open")end, { complex = true })
mp.add_key_binding(nil, "select-aid", function (t) if t.event == "up" or t.event == "repeat" then return end select_track("aid", "audio", "Select an audio track:", "No available audio tracks.", t.arg == "keep-open")end, { complex = true })
mp.add_key_binding(nil, "select-vid", function (t) if t.event == "up" or t.event == "repeat" then return end select_track("vid", "video", "Select a video track:", "No available video tracks.", t.arg == "keep-open")end, { complex = true })
local function format_time(t, duration) local fmt = math.max(t, duration) >= 60 * 60 and "%H:%M:%S" or "%M:%S" return mp.format_time(t, fmt)end
mp.add_key_binding(nil, "select-chapter", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open(default_item) local chapters = {} local current_chapter = mp.get_property_native("chapter")
if current_chapter == nil then show_warning("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 or (current_chapter > -1 and current_chapter + 1), keep_open = keep_open, submit = function (chapter) mp.set_property("chapter", chapter - 1) if keep_open then open(chapter) end end, }) end open()end, { complex = true })
local function format_edition(edition) return edition.title or ("Edition " .. edition.id + 1)end
mp.add_key_binding(nil, "select-edition", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open(default_item) local edition_list = mp.get_property_native("edition-list")
if edition_list == nil or #edition_list < 2 then show_warning("No available editions.") return end
local editions = {} local current_edition = mp.get_property_native("current-edition")
for i, edition in ipairs(edition_list) do editions[i] = format_edition(edition) end
input.select({ prompt = "Select an edition:", items = editions, default_item = default_item or (current_edition > -1 and current_edition + 1), keep_open = keep_open, submit = function (edition) mp.set_property("edition", edition - 1) if keep_open then open(edition) end end, }) end open()end, { complex = true })
local function select_subtitle_line(secondary, keep_open) local function open(default_item) local lines = mp.get_property_native(secondary .. "sub-lines")
if not lines then show_warning("Subtitle lines could not be retrieved.") return end
local items = {} local times = {} local delay = mp.get_property_native(secondary .. "sub-delay") local time_pos = mp.get_property_native("time-pos") - delay local duration = mp.get_property_native("duration", math.huge) local nested_call = default_item ~= nil
for _, line in ipairs(lines) do if not nested_call and line.start <= time_pos then default_item = #items + 1 end
for text in line.text:gmatch("[^\n]+") do items[#items + 1] = format_time(line.start, duration) .. " " .. text times[#times + 1] = line.start end
if line.text == "" then items[#items + 1] = format_time(line.start, duration) times[#times + 1] = line.start end end
input.select({ prompt = "Select a line to seek to:", items = items, default_item = default_item, keep_open = keep_open, submit = function (i) -- 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", times[i] + delay, "absolute") if keep_open then open(i) end end, }) end open()end
mp.add_key_binding(nil, "select-subtitle-line", function (t) if t.event == "up" or t.event == "repeat" then return end select_subtitle_line("", t.arg == "keep-open")end, { complex = true })mp.add_key_binding(nil, "select-secondary-subtitle-line", function (t) if t.event == "up" or t.event == "repeat" then return end select_subtitle_line("secondary-", t.arg == "keep-open")end, { complex = true })
local function format_audio_device(device) return device.name .. " (" .. device.description .. ")"end
mp.add_key_binding(nil, "select-audio-device", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open(default_item) 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")
if #devices == 0 then show_warning("No available audio devices.") return end
for i, device in ipairs(devices) do items[i] = format_audio_device(device)
if not default_item and device.name == selected_device then default_item = i end end
input.select({ prompt = "Select an audio device:", items = items, default_item = default_item, keep_open = keep_open, submit = function (id) mp.set_property("audio-device", devices[id].name) if keep_open then open(id) end end, }) end open()end, { complex = true })
local function format_history_entry(entry) local status status, entry.time = pcall(os.date, options.history_date_format, entry.time)
if not status then mp.msg.warn(entry.time) end
local item = "(" .. entry.time .. ") "
if entry.title then return item .. entry.title .. " (" .. entry.path .. ")" end
if entry.path:find("://") then return item .. entry.path end
local directory, filename = utils.split_path(entry.path)
return item .. filename .. " (" .. directory .. ")"end
mp.add_key_binding(nil, "select-watch-history", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local function open_history() local history_file_path = mp.command_native( {"expand-path", mp.get_property("watch-history-path")}) local history_file, error_message = io.open(history_file_path) if not history_file then show_warning(mp.get_property_native("save-watch-history") and error_message or "Enable --save-watch-history to jump to recently played files.") return end
local all_entries = {} local line_num = 1 for line in history_file:lines() do local entry = utils.parse_json(line) if entry and entry.path then all_entries[#all_entries + 1] = entry else mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num) end line_num = line_num + 1 end history_file:close()
local entries = {} local items = {} local seen = {}
for i = #all_entries, 1, -1 do local entry = all_entries[i] if not seen[entry.path] or not options.hide_history_duplicates then seen[entry.path] = true entries[#entries + 1] = entry items[#items + 1] = format_history_entry(entry) end end
items[#items+1] = "Clear history"
input.select({ prompt = "Select a file:", items = items, keep_open = keep_open, submit = function (i) if entries[i] then mp.commandv("loadfile", entries[i].path) return end
input.select({ prompt = "Are you sure you want to clear the history?", items = {"No", "Yes"}, submit = function (j) if j == 2 then error_message = select(2, os.remove(history_file_path)) if error_message then show_error(error_message) else mp.osd_message("History cleared.") end else open_history() end end })
end, }) end open_history()end, { complex = true })
mp.add_key_binding(nil, "select-watch-later", function (t) if t.event == "up" or t.event == "repeat" then return end local keep_open = t.arg == "keep-open" local watch_later_dir = mp.get_property("current-watch-later-dir")
if not watch_later_dir then show_warning("No watch later files found.") return end
local watch_later_files = {}
for i, file in ipairs(utils.readdir(watch_later_dir, "files") or {}) do watch_later_files[i] = watch_later_dir .. "/" .. file end
if #watch_later_files == 0 then show_warning("No watch later files found.") return end
local files = {} for _, watch_later_file in pairs(watch_later_files) do local file_handle = io.open(watch_later_file) if file_handle then local line = file_handle:read() if line and line ~= "# redirect entry" and line:find("^#") then files[#files + 1] = {line:sub(3), utils.file_info(watch_later_file).mtime} end file_handle:close() end end
if #files == 0 then show_warning(mp.get_property_native("write-filename-in-watch-later-config") and "No watch later files found." or "Enable --write-filename-in-watch-later-config to select recent files.") return end
table.sort(files, function (i, j) return i[2] > j[2] end)
local items = {} for i, file in ipairs(files) do items[i] = os.date("(%Y-%m-%d) ", file[2]) .. file[1] end
input.select({ prompt = "Select a file:", items = items, keep_open = keep_open, submit = function (i) mp.commandv("loadfile", files[i][1]) end, })end, { complex = true })
local function get_active_bindings() 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) ) and not binding.section:find("^input_forced_") -- OSC sections and binding.section ~= "input" and binding.section ~= "window-controls" then bindings[binding.key] = binding end end
return bindingsend
mp.add_key_binding(nil, "select-binding", function (t) if t.event == "up" or t.event == "repeat" then return end local items = {} for _, binding in pairs(get_active_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, keep_open = t.arg == "keep-open", submit = function (i) mp.command(items[i]:gsub("^.- ", "")) end, })end, { complex = true })
local function add_property(properties, property, value) value = value or mp.get_property_native(property)
if type(value) == "table" and next(value) then if type(next(value)) == "number" then for i, val in ipairs(value) do add_property(properties, property .. "/" .. (i - 1), val) end else for key, val in pairs(value) do add_property(properties, property .. "/" .. key, val) end end else if type(value) == "boolean" then value = value and "yes" or "no" else value = utils.to_string(value) end properties[#properties + 1] = property .. ": " .. value endend
mp.add_key_binding(nil, "show-properties", function (t) if t.event == "up" or t.event == "repeat" then return end local 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(properties, property) end
mp.set_property("msg-level", msg_level_backup)
add_property(properties, "current-tracks/audio") add_property(properties, "current-tracks/video") add_property(properties, "current-tracks/sub") add_property(properties, "current-tracks/sub2")
table.sort(properties)
input.select({ prompt = "Inspect a property:", items = properties, keep_open = t.arg == "keep-open", 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, { complex = true })
local function system_open(path) local args if platform == "windows" then args = {"rundll32", "url.dll,FileProtocolHandler", path} elseif platform == "darwin" then args = {"open", path} else args = {"gio", "open", path} end
mp.commandv("run", unpack(args))end
local function edit_config_file(filename) if not mp.get_property_bool("config") then show_warning("Editing config files with --no-config is not supported.") return end
local path = mp.find_config_file(filename)
if not path then path = mp.command_native({"expand-path", "~~/" .. filename}) local file_handle, error_message = io.open(path, "w")
if not file_handle then show_error(error_message) return end
file_handle:close() end
system_open(path)end
mp.add_key_binding(nil, "edit-config-file", function () edit_config_file("mpv.conf")end)
mp.add_key_binding(nil, "edit-input-conf", function () edit_config_file("input.conf")end)
mp.add_key_binding(nil, "open-docs", function () system_open("https://mpv.io/manual/")end)
mp.add_key_binding(nil, "open-chat", function () system_open("https://web.libera.chat/#mpv")end)
mp.add_key_binding(nil, "menu", function () local sub_track_count = 0 local audio_track_count = 0 local video_track_count = 0 local text_sub_selected = false local is_disc = mp.get_property("current-demuxer") == "disc"
local image_sub_codecs = to_map({"dvb_subtitle", "dvd_subtitle", "hdmv_pgs_subtitle"})
for _, track in pairs(mp.get_property_native("track-list")) do if track.type == "sub" then sub_track_count = sub_track_count + 1
if track["main-selection"] == 0 and not image_sub_codecs[track.codec] then text_sub_selected = true end elseif track.type == "audio" then audio_track_count = audio_track_count + 1 elseif track.type == "video" then video_track_count = video_track_count + 1 end end
local menu = { {"Subtitles", "script-binding select/select-sid", sub_track_count > 0}, {"Secondary subtitles", "script-binding select/select-secondary-sid", sub_track_count > 1}, {"Subtitle lines", "script-binding select/select-subtitle-line", text_sub_selected}, {"Audio tracks", "script-binding select/select-aid", audio_track_count > 1}, {"Video tracks", "script-binding select/select-vid", video_track_count > 1}, {"Playlist", "script-binding select/select-playlist", mp.get_property_native("playlist-count") > 1}, {"Chapters", "script-binding select/select-chapter", mp.get_property("chapter")}, {is_disc and "Titles" or "Editions", "script-binding select/select-edition", mp.get_property_native("edition-list/count", 0) > 1}, {"Audio devices", "script-binding select/select-audio-device", audio_track_count > 0}, {"Key bindings", "script-binding select/select-binding", true}, {"History", "script-binding select/select-watch-history", true}, {"Watch later", "script-binding select/select-watch-later", true}, {"Playback statistics", "script-binding stats/display-page-1-toggle", true}, {"File information", "script-binding stats/display-page-5-toggle", mp.get_property("filename")}, {"Edit config file", "script-binding select/edit-config-file", true}, {"Edit key bindings", "script-binding select/edit-input-conf", true}, {"Help", "script-binding stats/display-page-4-toggle", true}, {"Online documentation", "script-binding select/open-docs", true}, {"Support", "script-binding select/open-chat", true}, }
local labels = {} local commands = {}
for _, entry in ipairs(menu) do if entry[3] then labels[#labels + 1] = entry[1] commands[#commands + 1] = entry[2] end end
input.select({ items = labels, keep_open = true, submit = function (i) mp.command(commands[i])
if not commands[i]:find("^script%-binding select/select") then input.terminate() end end, })end)
local menu = {} -- contains wrappers of menu_data's itemslocal menu_data = {}local observed_properties = {}local property_cache = {}local active_bindings = {}local property_setlocal property_items = {}local have_dirty_items = falselocal current_item
local function on_property_change(name, value) property_cache[name] = value
if property_items[name] then for item, _ in pairs(property_items[name]) do item.dirty = true end have_dirty_items = true endend
local none_getters = { -- Only retrieve playlist when playlist-count is not huge to not kill -- performance. playlist is observed with type none to receive every -- MP_EVENT_CHANGE_PLAYLIST. playlist = function () if mp.get_property_native("playlist-count") < 100 then return mp.get_property_native("playlist") end end,}
local function on_none_property_change(name) on_property_change(name, none_getters[name]())end
local function is_known_property(name) if not property_set then property_set = to_map(mp.get_property_native("property-list")) end
return property_set[name] ~= nilend
function _G.get(name, default) if not observed_properties[name] then local result, err = (none_getters[name] or mp.get_property_native)(name)
if err == "property not found" and not is_known_property(name:match("^([^/]+)")) then mp.msg.error("Property '" .. name .. "' was not found.") return default end
observed_properties[name] = true property_cache[name] = result
if none_getters[name] then mp.observe_property(name, "none", on_none_property_change) else mp.observe_property(name, "native", on_property_change) end end
if current_item then if not property_items[name] then property_items[name] = {} end
property_items[name][current_item] = true end
if property_cache[name] == nil then return default end
return property_cache[name]end
local function magic_get(name) return get(name:gsub("_", "-"), nil)end
local evil_magic = {}setmetatable(evil_magic, { __index = function(_, key) if _G[key] ~= nil then return _G[key] end
return magic_get(key) end,})
_G.p = {}setmetatable(p, { __index = function(_, key) return magic_get(key) end,})
local function compile_condition(chunk, chunkname) chunk = "return " .. chunk chunkname = 'Menu entry "' .. chunkname .. '"'
local compiled_chunk, err
-- luacheck: push -- luacheck: ignore setfenv loadstring if setfenv then -- lua 5.1 compiled_chunk, err = loadstring(chunk, chunkname) if compiled_chunk then setfenv(compiled_chunk, evil_magic) end else -- lua 5.2 compiled_chunk, err = load(chunk, chunkname, "t", evil_magic) end -- luacheck: pop
if not compiled_chunk then mp.msg.error(chunkname .. " : " .. err) compiled_chunk = function() return false end end
return compiled_chunkend
local function evaluate_condition(chunk, chunkname) local status, result status, result = pcall(chunk)
if not status then mp.msg.verbose(chunkname .. " error on evaluating: " .. result) return false end
return not not resultend
local function toggle_state(states, state, add) for i, existing_state in ipairs(states) do if existing_state == state then if add then return end
table.remove(states, i) end end
if add then states[#states + 1] = state endend
local function on_idle() if not have_dirty_items then return end
have_dirty_items = false
for _, item in pairs(menu) do if item.dirty then current_item = item item:update() current_item = nil item.dirty = false end end
mp.set_property_native("menu-data", menu_data)end
-- quote string and escape it in JSON-stylelocal function quote(str) return utils.format_json(str or "")end
local function clamp_submenu(submenu, max, cmd) if #submenu <= max then return submenu end
local mid = 1 for i, item in pairs(submenu) do if item.state then mid = i break end end
local offset = math.floor(max / 2) local first = mid + 1 - offset local last = mid + offset
if first < 1 then first = 1 last = max end
if last > #submenu then first = math.max(#submenu - max + 1, 1) last = #submenu end
local clamped = {}
if first > 1 then clamped[1] = { title = "…", cmd = cmd, shortcut = first - 1 .. " more", } end
for i = first, last do clamped[#clamped + 1] = submenu[i] end
if last < #submenu then clamped[#clamped + 1] = { title = "…", cmd = cmd, shortcut = #submenu - last .. " more", } end
return clampedend
local function playlist() local show = get("osd-playlist-entry")
if not get("playlist") then return end
local items = {}
for i, entry in ipairs(get("playlist")) do items[i] = { title = format_playlist_entry(entry, show):gsub("&", "&&"), cmd = "playlist-play-index " .. (i - 1) }
if entry.current then items[i].state = {"checked"} end end
return clamp_submenu(items, options.max_playlist_items, "script-binding select/select-playlist")end
local function tracks(property, track_type) local items = {} local track_list = get("track-list") local last_type
local positions = {video = 0, audio = 1, sub = 2} table.sort(track_list, function (i, j) if i.type ~= j.type then return positions[i.type] < positions[j.type] end
return i.id < j.id end)
for _, track in ipairs(track_list) do if not track_type or track.type == track_type then if last_type and track.type ~= last_type then items[#items + 1] = { type = "separator" } end last_type = track.type
items[#items + 1] = { title = (track_type and "" or format_track_type(track)) .. -- Remove the circles since checkmarks are already added. format_track(track):sub(5):gsub("&", "&&"), cmd = "set " .. (property or track.type) .. " " .. track.id, }
if track.selected then items[#items].cmd = "set " .. (property or track.type) .. " no" items[#items].state = {"checked"} end end end
return itemsend
local function chapters() local items = {} local current_chapter = get("chapter", -1) local duration = mp.get_property_native("duration", math.huge)
for i, chapter in ipairs(get("chapter-list")) do items[i] = { title = chapter.title:gsub("&", "&&"), cmd = "set chapter " .. (i - 1), shortcut = format_time(chapter.time, duration), }
if i == current_chapter + 1 then items[i].state = {"checked"} end end
return itemsend
local function editions() local items = {} local current_edition = get("current-edition", -1)
for i, edition in ipairs(get("edition-list", {})) do items[i] = { title = format_edition(edition):gsub("&", "&&"), cmd = "set edition " .. (i - 1), }
if i == current_edition + 1 then items[i].state = {"checked"} end end
return itemsend
local function audio_devices() local items = {} local selected_device = get("audio-device")
for i, device in ipairs(get("audio-device-list")) do items[i] = { title = format_audio_device(device):gsub("&", "&&"), cmd = "set audio-device " .. quote(device.name), }
if device.name == selected_device then items[i].state = {"checked"} end end
return itemsend
local function profiles() local builtin_profiles = to_map({ "box", "builtin-pseudo-gui", "default", "encoding", "fast", "gpu-hq", "high-quality", "libmpv", "low-latency", "osd-box", "pseudo-gui", "sub-box", "sw-fast" })
local user_profiles = {} for _, profile in ipairs(get("profile-list")) do if not builtin_profiles[profile.name] then user_profiles[#user_profiles + 1] = profile.name end end table.sort(user_profiles)
local items = {}
for i, profile in ipairs({ "fast", "high-quality", "osd-box", "sub-box", "box", }) do items[i] = { title = profile, cmd = "apply-profile " .. profile, } end
if user_profiles[1] then items[#items + 1] = { type = "separator" } end
for _, profile in ipairs(user_profiles) do items[#items + 1] = { title = profile:gsub("&", "&&"), cmd = "apply-profile " .. quote(profile), } end
return itemsend
local builtin_submenus = { ["$playlist"] = playlist, ["$tracks"] = function () return tracks() end, ["$video-tracks"] = function () return tracks("video", "video") end, ["$audio-tracks"] = function () return tracks("audio", "audio") end, ["$sub-tracks"] = function () return tracks("sub", "sub") end, ["$secondary-sub-tracks"] = function () return tracks("secondary-sid", "sub") end, ["$chapters"] = chapters, ["$editions"] = editions, ["$audio-devices"] = audio_devices, ["$profiles"] = profiles,}
local submenu_commands = { ["$playlist"] = "script-binding select/select-playlist", ["$tracks"] = "script-binding select/select-track", ["$video-tracks"] = "script-binding select/select-vid", ["$audio-tracks"] = "script-binding select/select-aid", ["$sub-tracks"] = "script-binding select/select-sid", ["$secondary-sub-tracks"] = "script-binding select/select-secondary-sid", ["$chapters"] = "script-binding select/select-chapter", ["$editions"] = "script-binding select/select-edition", ["$audio-devices"] = "script-binding select/select-audio-device",}
local function get_shortcut(cmd) local shortcuts = {} local uncommon_keys = to_map({ "MBTN_BACK", "MBTN_FORWARD", "POWER", "PLAY", "PAUSE", "PLAYPAUSE", "PLAYONLY", "PAUSEONLY", "STOP", "FORWARD", "REWIND", "NEXT", "PREV", "VOLUME_UP", "VOLUME_DOWN", "MUTE", "CLOSE_WIN", })
for _, binding in pairs(active_bindings) do if binding.cmd == cmd and not uncommon_keys[binding.key] and not binding.key:find("KP_") then shortcuts[#shortcuts + 1] = binding.key end end
return table.concat(shortcuts, ",")end
local function update_builtin_submenu(item) item.item.submenu = builtin_submenus[item.builtin_submenu]()
-- With huge playlists, make the menu item open the playlist menu -- instead to not kill performance. if not item.item.submenu then item.item.cmd = submenu_commands[item.builtin_submenu] item.item.type = nil return end
item.item.cmd = nil item.item.type = "submenu"
local min = item.builtin_submenu == "$editions" and 2 or 1 item.item.state = #item.item.submenu < min and {"disabled"} or {}end
local function update_state(item) for state, compiled_condition in pairs(item.compiled_conditions) do toggle_state(item.item.state, state, evaluate_condition(compiled_condition, item.item.title)) endend
local function parse_menu_item(line) local tokens = {} local separator = "\t+" for token in line:gmatch("(.-)" .. separator) do tokens[#tokens + 1] = token end tokens[#tokens + 1] = line:gsub(".*" .. separator, "")
if tokens[1] == "" then return { type = "separator" } end
local item = { item = { title = tokens[1], state = {}, }, compiled_conditions = {}, }
current_item = item
if builtin_submenus[tokens[2]] then item.builtin_submenu = tokens[2] item.item.shortcut = get_shortcut(submenu_commands[item.builtin_submenu]) item.update = update_builtin_submenu item:update() return item end
local state_start = 3 for _, state in pairs({"checked", "disabled", "hidden"}) do if not tokens[2] or tokens[2]:find("^%s*" .. state .. "=") then state_start = 2 break end end
if state_start == 2 then item.item.type = "submenu" item.item.submenu = {} else item.item.cmd = tokens[2] item.item.shortcut = get_shortcut(tokens[2]) end
for i = state_start, #tokens do local state, condition = tokens[i]:match("(%S-)=(.*)")
if not state then return false end
item.compiled_conditions[state] = compile_condition(condition, tokens[1]) if evaluate_condition(item.compiled_conditions[state], tokens[1]) then table.insert(item.item.state, state) end end
item.update = update_state
return itemend
local function get_menu_conf() local menu_conf local file_handle = io.open(mp.command_native({"expand-path", options.menu_conf_path})) if file_handle then menu_conf = file_handle:read("*a") file_handle:close() else menu_conf = mp.get_property("default-menu") end
local lines = {} for line in menu_conf:gmatch("(.-)\r?\n") do lines[#lines + 1] = line end
return linesend
local function parse_menu_conf(_, vo_configured) if not vo_configured then return end
mp.unobserve_property(parse_menu_conf)
active_bindings = get_active_bindings()
local lines = get_menu_conf() local last_leading_whitespace = "" local menus_by_depth = { [""] = menu_data }
for i, line in ipairs(lines) do local leading_whitespace = line:match("^%s*") local item = parse_menu_item(line:gsub("^%s*", ""))
if not item then show_error("menu.conf is malformed: " .. line .. " contains tabs not used as separators") return end
if item.item then menu[#menu + 1] = item item = item.item end
if #leading_whitespace > #last_leading_whitespace then local last_menu = menus_by_depth[last_leading_whitespace]
if not last_menu[#last_menu].submenu then show_error("menu.conf is malformed: " .. line .. " has leading whitespace but no parent menu was defined") return end
menus_by_depth[leading_whitespace] = last_menu[#last_menu].submenu end
if line == "" then -- Determine the depth of the separator from the next line. if lines[i + 1] then -- ignore newlines at the end table.insert(menus_by_depth[lines[i + 1]:match("%s*")], item) end else table.insert(menus_by_depth[leading_whitespace], item) last_leading_whitespace = leading_whitespace end end
active_bindings = nil current_item = nil
mp.register_idle(on_idle)end
if options.populate_menu_data then mp.observe_property("vo-configured", "native", parse_menu_conf)end
mp.add_key_binding(nil, "context-menu", function (info) if info.event == "repeat" then return end
if not info.is_mouse then if info.event == "down" or info.event == "press" then mp.command(mp.get_property_native("load-context-menu") and "script-message-to context_menu open" or "context-menu") end elseif mp.get_property_native("load-context-menu") then mp.commandv("script-message-to", "context_menu", info.event == "down" and "open" or "select") elseif info.event == (platform == "windows" and "up" or "down") then mp.command("context-menu") endend, { complex = true })