Overlay 2.0 (#12)

This commit is contained in:
2026-03-01 02:36:51 -08:00
committed by GitHub
parent 45df3c466b
commit 44c7761c7c
397 changed files with 15139 additions and 7127 deletions

View File

@@ -3,9 +3,12 @@ local function run_plugin_scenario(config)
local recorded = {
async_calls = {},
sync_calls = {},
script_messages = {},
events = {},
osd = {},
logs = {},
property_sets = {},
}
local function make_mp_stub()
@@ -15,6 +18,9 @@ local function run_plugin_scenario(config)
if name == "platform" then
return config.platform or "linux"
end
if name == "input-ipc-server" then
return config.input_ipc_server or ""
end
if name == "filename/no-ext" then
return config.filename_no_ext or ""
end
@@ -34,7 +40,12 @@ local function run_plugin_scenario(config)
return config.chapter_list or {}
end
function mp.get_script_directory()
return "plugin/subminer"
end
function mp.command_native(command)
recorded.sync_calls[#recorded.sync_calls + 1] = command
local args = command.args or {}
if args[1] == "ps" then
return {
@@ -44,6 +55,13 @@ local function run_plugin_scenario(config)
}
end
if args[1] == "curl" then
local url = args[#args] or ""
if type(url) == "string" and url:find("myanimelist", 1, true) then
return { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" }
end
if type(url) == "string" and url:find("api.aniskip.com", 1, true) then
return { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" }
end
return { status = 0, stdout = "{}", stderr = "" }
end
return { status = 0, stdout = "", stderr = "" }
@@ -52,6 +70,22 @@ local function run_plugin_scenario(config)
function mp.command_native_async(command, callback)
recorded.async_calls[#recorded.async_calls + 1] = command
if callback then
local args = command.args or {}
if args[1] == "ps" then
callback(true, { status = 0, stdout = config.process_list or "", stderr = "" }, nil)
return
end
if args[1] == "curl" then
local url = args[#args] or ""
if type(url) == "string" and url:find("myanimelist", 1, true) then
callback(true, { status = 0, stdout = config.mal_lookup_stdout or "{}", stderr = "" }, nil)
return
end
if type(url) == "string" and url:find("api.aniskip.com", 1, true) then
callback(true, { status = 0, stdout = config.aniskip_stdout or "{}", stderr = "" }, nil)
return
end
end
callback(true, { status = 0, stdout = "", stderr = "" }, nil)
end
end
@@ -67,17 +101,28 @@ local function run_plugin_scenario(config)
end
function mp.add_key_binding(_keys, _name, _fn) end
function mp.register_event(_name, _fn) end
function mp.register_event(name, fn)
if recorded.events[name] == nil then
recorded.events[name] = {}
end
recorded.events[name][#recorded.events[name] + 1] = fn
end
function mp.add_hook(_name, _prio, _fn) end
function mp.observe_property(_name, _kind, _fn) end
function mp.osd_message(message, _duration)
recorded.osd[#recorded.osd + 1] = message
end
function mp.set_osd_ass(...) end
function mp.get_time()
return 0
end
function mp.commandv(...) end
function mp.set_property_native(...) end
function mp.set_property_native(name, value)
recorded.property_sets[#recorded.property_sets + 1] = {
name = name,
value = value,
}
end
function mp.get_script_name()
return "subminer"
end
@@ -90,8 +135,8 @@ local function run_plugin_scenario(config)
local utils = {}
function options.read_options(target, _name)
if config.socket_path then
target.socket_path = config.socket_path
for key, value in pairs(config.option_overrides or {}) do
target[key] = value
end
end
@@ -108,7 +153,35 @@ local function run_plugin_scenario(config)
return table.concat(parts, "/")
end
function utils.parse_json(_json)
function utils.parse_json(json)
if json == "__MAL_FOUND__" then
return {
categories = {
{
items = {
{
id = 99,
name = "Sample Show",
},
},
},
},
}, nil
end
if json == "__ANISKIP_FOUND__" then
return {
found = true,
results = {
{
skip_type = "op",
interval = {
start_time = 12.3,
end_time = 45.6,
},
},
},
}, nil
end
return {}, nil
end
@@ -149,7 +222,7 @@ local function run_plugin_scenario(config)
return utils
end
local ok, err = pcall(dofile, "plugin/subminer.lua")
local ok, err = pcall(dofile, "plugin/subminer/main.lua")
if not ok then
return nil, err, recorded
end
@@ -168,6 +241,82 @@ local function find_start_call(async_calls)
local args = call.args or {}
for i = 1, #args do
if args[i] == "--start" then
return call
end
end
end
return nil
end
local function count_start_calls(async_calls)
local count = 0
for _, call in ipairs(async_calls) do
local args = call.args or {}
for _, value in ipairs(args) do
if value == "--start" then
count = count + 1
break
end
end
end
return count
end
local function find_control_call(async_calls, flag)
for _, call in ipairs(async_calls) do
local args = call.args or {}
local has_flag = false
local has_start = false
for _, value in ipairs(args) do
if value == flag then
has_flag = true
elseif value == "--start" then
has_start = true
end
end
if has_flag and not has_start then
return call
end
end
return nil
end
local function call_has_arg(call, target)
local args = (call and call.args) or {}
for _, value in ipairs(args) do
if value == target then
return true
end
end
return false
end
local function has_sync_command(sync_calls, executable)
for _, call in ipairs(sync_calls) do
local args = call.args or {}
if args[1] == executable then
return true
end
end
return false
end
local function has_async_command(async_calls, executable)
for _, call in ipairs(async_calls) do
local args = call.args or {}
if args[1] == executable then
return true
end
end
return false
end
local function has_async_curl_for(async_calls, needle)
for _, call in ipairs(async_calls) do
local args = call.args or {}
if args[1] == "curl" then
local url = args[#args] or ""
if type(url) == "string" and url:find(needle, 1, true) then
return true
end
end
@@ -175,11 +324,50 @@ local function find_start_call(async_calls)
return false
end
local function has_property_set(property_sets, name, value)
for _, call in ipairs(property_sets) do
if call.name == name and call.value == value then
return true
end
end
return false
end
local function has_osd_message(messages, target)
for _, message in ipairs(messages) do
if message == target then
return true
end
end
return false
end
local function count_osd_message(messages, target)
local count = 0
for _, message in ipairs(messages) do
if message == target then
count = count + 1
end
end
return count
end
local function fire_event(recorded, name)
local listeners = recorded.events[name] or {}
for _, listener in ipairs(listeners) do
listener()
end
end
local binary_path = "/tmp/subminer-binary"
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
},
files = {
[binary_path] = true,
},
@@ -187,7 +375,227 @@ do
assert_true(recorded ~= nil, "plugin failed to load for cold-start scenario: " .. tostring(err))
assert_true(recorded.script_messages["subminer-start"] ~= nil, "subminer-start script message not registered")
recorded.script_messages["subminer-start"]("texthooker=no")
assert_true(find_start_call(recorded.async_calls), "expected cold-start to invoke --start command when process is absent")
assert_true(find_start_call(recorded.async_calls) ~= nil, "expected cold-start to invoke --start command when process is absent")
assert_true(
not has_sync_command(recorded.sync_calls, "ps"),
"expected cold-start start command to avoid synchronous process list scan"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
},
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for non-subminer file-load scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
assert_true(not has_sync_command(recorded.sync_calls, "ps"), "file-loaded should avoid synchronous process checks")
assert_true(not has_sync_command(recorded.sync_calls, "curl"), "file-loaded should avoid synchronous AniSkip network calls")
assert_true(
not has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"),
"file-loaded without SubMiner context should skip AniSkip MAL lookup"
)
assert_true(
not has_async_curl_for(recorded.async_calls, "api.aniskip.com"),
"file-loaded without SubMiner context should skip AniSkip API lookup"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
},
media_title = "Sample Show S01E01",
mal_lookup_stdout = "__MAL_FOUND__",
aniskip_stdout = "__ANISKIP_FOUND__",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for script-message AniSkip scenario: " .. tostring(err))
assert_true(recorded.script_messages["subminer-aniskip-refresh"] ~= nil, "subminer-aniskip-refresh script message not registered")
recorded.script_messages["subminer-aniskip-refresh"]()
assert_true(not has_sync_command(recorded.sync_calls, "curl"), "AniSkip refresh should not perform synchronous curl calls")
assert_true(has_async_command(recorded.async_calls, "curl"), "AniSkip refresh should perform async curl calls")
assert_true(
has_async_curl_for(recorded.async_calls, "myanimelist.net/search/prefix.json"),
"AniSkip refresh should perform MAL lookup even when app is not running"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "no",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for visible auto-start scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
local start_call = find_start_call(recorded.async_calls)
assert_true(start_call ~= nil, "auto-start should issue --start command")
assert_true(
not call_has_arg(start_call, "--show-visible-overlay"),
"auto-start should keep --start command free of --show-visible-overlay"
)
assert_true(
not call_has_arg(start_call, "--hide-visible-overlay"),
"auto-start should keep --start command free of --hide-visible-overlay"
)
assert_true(
find_control_call(recorded.async_calls, "--show-visible-overlay") ~= nil,
"auto-start with visible overlay enabled should issue a separate --show-visible-overlay command"
)
assert_true(
not has_property_set(recorded.property_sets, "pause", true),
"auto-start visible overlay should not force pause without explicit pause-until-ready option"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for duplicate auto-start scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
fire_event(recorded, "file-loaded")
assert_true(
count_start_calls(recorded.async_calls) == 1,
"duplicate file-loaded events should not issue duplicate --start commands while overlay is already running"
)
assert_true(
count_osd_message(recorded.osd, "SubMiner: Already running") == 0,
"duplicate auto-start events should not show Already running OSD"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
auto_start_pause_until_ready = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for pause-until-ready scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
assert_true(
has_property_set(recorded.property_sets, "pause", true),
"pause-until-ready auto-start should pause mpv before overlay ready"
)
assert_true(recorded.script_messages["subminer-autoplay-ready"] ~= nil, "subminer-autoplay-ready script message not registered")
recorded.script_messages["subminer-autoplay-ready"]()
assert_true(
has_property_set(recorded.property_sets, "pause", false),
"autoplay-ready script message should resume mpv playback"
)
assert_true(
has_osd_message(recorded.osd, "SubMiner: Loading subtitle annotations..."),
"pause-until-ready auto-start should show loading OSD message"
)
assert_true(
has_osd_message(recorded.osd, "SubMiner: Subtitle annotations loaded"),
"autoplay-ready should show loaded OSD message"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "no",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for hidden auto-start scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
local start_call = find_start_call(recorded.async_calls)
assert_true(start_call ~= nil, "auto-start should issue --start command")
assert_true(
not call_has_arg(start_call, "--hide-visible-overlay"),
"auto-start should keep --start command free of --hide-visible-overlay"
)
assert_true(
not call_has_arg(start_call, "--show-visible-overlay"),
"auto-start should keep --start command free of --show-visible-overlay"
)
assert_true(
find_control_call(recorded.async_calls, "--hide-visible-overlay") ~= nil,
"auto-start with visible overlay disabled should issue a separate --hide-visible-overlay command"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "yes",
auto_start_visible_overlay = "yes",
socket_path = "/tmp/subminer-socket",
},
input_ipc_server = "/tmp/other.sock",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for mismatched socket auto-start scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
local start_call = find_start_call(recorded.async_calls)
assert_true(
start_call == nil,
"auto-start should be skipped when mpv input-ipc-server does not match configured socket_path"
)
assert_true(
not has_property_set(recorded.property_sets, "pause", true),
"pause-until-ready gate should not arm when socket_path does not match"
)
end
print("plugin start gate regression tests: OK")