summaryrefslogtreecommitdiffstats
path: root/player/lua/auto_profiles.lua
diff options
context:
space:
mode:
Diffstat (limited to 'player/lua/auto_profiles.lua')
-rw-r--r--player/lua/auto_profiles.lua208
1 files changed, 208 insertions, 0 deletions
diff --git a/player/lua/auto_profiles.lua b/player/lua/auto_profiles.lua
new file mode 100644
index 0000000000..167724344c
--- /dev/null
+++ b/player/lua/auto_profiles.lua
@@ -0,0 +1,208 @@
+-- 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
+ -- 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