1
0
mirror of https://github.com/mpv-player/mpv synced 2025-02-17 21:27:08 +00:00
mpv/player/lua/auto_profiles.lua
Guido Cella 5dc4047415 auto_profiles.lua: actually exit when no auto profiles are defined
Unsetting _G.mp_event_loop at the top level quits the script, but not
within callbacks. Use the new exit() function instead. Fixes e2284fba18.

This actually has an edge case since e2284fba18 where you can add auto
profiles only later with load-config-file and the script stays unloaded,
but it's still reasonable to quit if mpv.conf has no conditional
profiles. You could always explicitly set --load-auto-profiles=yes in
this case.
2024-11-02 14:03:02 +02:00

208 lines
6.5 KiB
Lua

-- Note: anything global is accessible by profile condition expressions.
local msg = require 'mp.msg'
local profiles = {}
local watched_properties = {} -- indexed by property name (used as a set)
local cached_properties = {} -- property name -> last known raw value
local properties_to_profiles = {} -- property name -> set of profiles using it
local have_dirty_profiles = false -- at least one profile is marked dirty
local pending_hooks = {} -- as set (keys only, meaningless values)
-- Used during evaluation of the profile condition, and should contain the
-- profile the condition is evaluated for.
local current_profile = nil
-- Cached set of all top-level mpv properities. Only used for extra validation.
local property_set = {}
for _, property in pairs(mp.get_property_native("property-list")) do
property_set[property] = true
end
local function evaluate(profile)
msg.verbose("Re-evaluating auto profile " .. profile.name)
current_profile = profile
local status, res = pcall(profile.cond)
current_profile = nil
if not status then
-- errors can be "normal", e.g. in case properties are unavailable
msg.verbose("Profile condition error on evaluating: " .. res)
res = false
end
res = not not res
if res ~= profile.status then
if res == true then
msg.info("Applying auto profile: " .. profile.name)
mp.commandv("apply-profile", profile.name)
elseif profile.status == true and profile.has_restore_opt then
msg.info("Restoring profile: " .. profile.name)
mp.commandv("apply-profile", profile.name, "restore")
end
end
profile.status = res
profile.dirty = false
end
local function on_property_change(name, val)
cached_properties[name] = val
-- Mark all profiles reading this property as dirty, so they get re-evaluated
-- the next time the script goes back to sleep.
local dependent_profiles = properties_to_profiles[name]
if dependent_profiles then
for profile, _ in pairs(dependent_profiles) do
assert(profile.cond) -- must be a profile table
profile.dirty = true
have_dirty_profiles = true
end
end
end
local function on_idle()
-- When events and property notifications stop, re-evaluate all dirty profiles.
if have_dirty_profiles then
for _, profile in ipairs(profiles) do
if profile.dirty then
evaluate(profile)
end
end
end
have_dirty_profiles = false
-- Release all hooks (the point was to wait until an idle event)
while true do
local h = next(pending_hooks)
if not h then
break
end
pending_hooks[h] = nil
h:cont()
end
end
local function on_hook(h)
h:defer()
pending_hooks[h] = true
end
function get(name, default)
-- Normally, we use the cached value only
if not watched_properties[name] then
watched_properties[name] = true
local res, err = mp.get_property_native(name)
-- Property has to not exist and the toplevel of property in the name must also
-- not have an existing match in the property set for this to be considered an error.
-- This allows things like user-data/test to still work.
if err == "property not found" and property_set[name:match("^([^/]+)")] == nil then
msg.error("Property '" .. name .. "' was not found.")
return default
end
cached_properties[name] = res
mp.observe_property(name, "native", on_property_change)
end
-- The first time the property is read we need add it to the
-- properties_to_profiles table, which will be used to mark the profile
-- dirty if a property referenced by it changes.
if current_profile then
local map = properties_to_profiles[name]
if not map then
map = {}
properties_to_profiles[name] = map
end
map[current_profile] = true
end
local val = cached_properties[name]
if val == nil then
val = default
end
return val
end
local function magic_get(name)
-- Lua identifiers can't contain "-", so in order to match with mpv
-- property conventions, replace "_" to "-"
name = string.gsub(name, "_", "-")
return get(name, nil)
end
local evil_magic = {}
setmetatable(evil_magic, {
__index = function(_, key)
-- interpret everything as property, unless it already exists as
-- a non-nil global value
local v = _G[key]
if type(v) ~= "nil" then
return v
end
return magic_get(key)
end,
})
p = {}
setmetatable(p, {
__index = function(_, key)
return magic_get(key)
end,
})
local function compile_cond(name, s)
local code, chunkname = "return " .. s, "profile " .. name .. " condition"
local chunk, err
-- luacheck: push
-- luacheck: ignore setfenv loadstring
if setfenv then -- lua 5.1
chunk, err = loadstring(code, chunkname)
if chunk then
setfenv(chunk, evil_magic)
end
else -- lua 5.2
chunk, err = load(code, chunkname, "t", evil_magic)
end
-- luacheck: pop
if not chunk then
msg.error("Profile '" .. name .. "' condition: " .. err)
chunk = function() return false end
end
return chunk
end
local function load_profiles(profiles_property)
for _, v in ipairs(profiles_property) do
local cond = v["profile-cond"]
if cond and #cond > 0 then
local profile = {
name = v.name,
cond = compile_cond(v.name, cond),
properties = {},
status = nil,
dirty = true, -- need re-evaluate
has_restore_opt = v["profile-restore"] and v["profile-restore"] ~= "default"
}
profiles[#profiles + 1] = profile
have_dirty_profiles = true
end
end
end
mp.observe_property("profile-list", "native", function (_, profiles_property)
profiles = {}
watched_properties = {}
cached_properties = {}
properties_to_profiles = {}
mp.unobserve_property(on_property_change)
load_profiles(profiles_property)
if #profiles < 1 and mp.get_property("load-auto-profiles") == "auto" then
exit()
return
end
on_idle() -- re-evaluate all profiles immediately
end)
mp.register_idle(on_idle)
for _, name in ipairs({"on_load", "on_preloaded", "on_before_start_file"}) do
mp.add_hook(name, 50, on_hook)
end