mpv/player/lua/auto_profiles.lua

209 lines
6.6 KiB
Lua
Raw Normal View History

-- 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
auto_profiles: try to distinguish invalid properties better 6e4a76db0862303ae7f0f9fd7bdddff128fbd2f0 attemped to reject invalid properties and print an error for users so they actually know that something is going wrong. This worked by simply checking if the property not found error is returned, but it is actually perfectly possible for a property to not be found (different than being unavailable just to be clear here) at first and then show up later. An example would be user-data which can be created at any time. It's also possible with subproperties of things like track-list where a new track could be added later. In light of this, let's soften the error checking logic here with a simple trick. mpv already keeps track of all toplevel properties and it can be easily retrieved with the "property-list" property, so just cache that. When we get a property not found error, instead of rejecting it, try to match it something in the property-list first. If we have a match, then consider the property valid and allow the script to behavior normally. If not, we reject it. This approach means property names that are obviously wrong like "fake-property-here" will reliably get rejected and something like "user-data/test" works as usual. The downside is that errors in the subproperty level are not caught, so something like "track-list/0/fake-property" would still be considered valid and the user gets no warning that this won't work. We'll just accept the compromise and hope this isn't too common. Fixes #11550.
2023-04-06 00:49:07 +00:00
-- 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)
auto_profiles: try to distinguish invalid properties better 6e4a76db0862303ae7f0f9fd7bdddff128fbd2f0 attemped to reject invalid properties and print an error for users so they actually know that something is going wrong. This worked by simply checking if the property not found error is returned, but it is actually perfectly possible for a property to not be found (different than being unavailable just to be clear here) at first and then show up later. An example would be user-data which can be created at any time. It's also possible with subproperties of things like track-list where a new track could be added later. In light of this, let's soften the error checking logic here with a simple trick. mpv already keeps track of all toplevel properties and it can be easily retrieved with the "property-list" property, so just cache that. When we get a property not found error, instead of rejecting it, try to match it something in the property-list first. If we have a match, then consider the property valid and allow the script to behavior normally. If not, we reject it. This approach means property names that are obviously wrong like "fake-property-here" will reliably get rejected and something like "user-data/test" works as usual. The downside is that errors in the subproperty level are not caught, so something like "track-list/0/fake-property" would still be considered valid and the user gets no warning that this won't work. We'll just accept the compromise and hope this isn't too common. Fixes #11550.
2023-04-06 00:49:07 +00:00
-- 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
-- make it exit immediately
_G.mp_event_loop = function() end
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