fix(jellyfin): fix overlay toggle sync, redirect reload, and AppImage bi

- Sync visible-overlay state back to plugin via script messages to avoid toggle/hide drift
- Collapse duplicate toggle events within 250ms to prevent hide-then-show on single keypress
- Preserve manual hide across Jellyfin path-changing redirects even when media-title drops
- Rearm managed subtitle defaults on path-changing redirects
- Route toggleVisibleOverlay session binding through plugin toggle instead of app-side IPC
- Show Linux/Hyprland overlay passively (showInactive) to avoid stealing mpv keyboard focus
- Fix AppImage binary resolution to prefer $APPIMAGE env over mounted inner binary
- Add stats window layer management so delete/update dialogs appear above stats window
- Fix Jellyfin remote progress sync during Linux websocket reconnect windows
This commit is contained in:
2026-05-23 01:45:09 -07:00
parent 49a94579b6
commit afe1731514
46 changed files with 1472 additions and 79 deletions
+25
View File
@@ -68,6 +68,31 @@ local function create_binary_module(config)
return binary
end
do
local appimage_path = "/home/tester/.local/bin/SubMiner.AppImage"
local mounted_binary_path = "/tmp/.mount_SubMiner/SubMiner"
local resolved = with_env({
APPIMAGE = appimage_path,
}, function()
local binary = create_binary_module({
is_windows = false,
binary_path = mounted_binary_path,
entries = {
[appimage_path] = "file",
[mounted_binary_path] = "file",
},
})
return binary.find_binary()
end)
assert_equal(
resolved,
appimage_path,
"linux resolver should prefer APPIMAGE over the mounted AppImage inner binary"
)
end
do
local binary = create_binary_module({
is_windows = true,
+17
View File
@@ -23,6 +23,7 @@ local recorded = {
async_calls = {},
mpv_commands = {},
osd = {},
overlay_toggles = 0,
}
local mp = {}
@@ -68,6 +69,14 @@ local ctx = {
return {
numericSelectionTimeoutMs = 3000,
bindings = {
{
key = {
code = "KeyO",
modifiers = { "alt", "shift" },
},
actionType = "session-action",
actionId = "toggleVisibleOverlay",
},
{
key = {
code = "KeyS",
@@ -253,6 +262,9 @@ local ctx = {
run_binary_command_async = function(args)
recorded.async_calls[#recorded.async_calls + 1] = args
end,
toggle_overlay = function()
recorded.overlay_toggles = recorded.overlay_toggles + 1
end,
},
environment = {
resolve_session_bindings_artifact_path = function()
@@ -318,6 +330,11 @@ local expected_cli_bindings = {
{ keys = "w", flag = "--mark-watched" },
}
local visible_overlay_toggle = find_binding("Alt+O")
assert_true(visible_overlay_toggle ~= nil, "visible overlay session binding should register")
visible_overlay_toggle.fn()
assert_true(recorded.overlay_toggles == 1, "visible overlay session binding should use plugin toggle")
for _, expected in ipairs(expected_cli_bindings) do
local binding = find_binding(expected.keys)
assert_true(binding ~= nil, "default session action should register " .. expected.keys)
+171 -3
View File
@@ -201,7 +201,7 @@ local function run_plugin_scenario(config)
end
function mp.set_osd_ass(...) end
function mp.get_time()
return 0
return config.now or 0
end
function mp.commandv(...) end
function mp.set_property_native(name, value)
@@ -623,16 +623,18 @@ local binary_path = "/tmp/subminer-binary"
local appimage_path = "/tmp/SubMiner.AppImage"
do
local recorded, err = run_plugin_scenario({
local scenario = {
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
},
now = 20,
files = {
[binary_path] = true,
},
})
}
local recorded, err = run_plugin_scenario(scenario)
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")
@@ -683,6 +685,125 @@ do
)
end
do
local 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",
path = "/media/jellyfin-app-toggle-initial.m3u8",
media_title = "Jellyfin App Toggle",
paused = true,
files = {
[binary_path] = true,
},
}
local recorded, err = run_plugin_scenario(scenario)
assert_true(recorded ~= nil, "plugin failed to load for app-side hide Jellyfin redirect: " .. tostring(err))
fire_event(recorded, "start-file")
fire_event(recorded, "file-loaded")
recorded.script_messages["subminer-visible-overlay-hidden"]()
fire_event(recorded, "end-file", { reason = "redirect" })
scenario.path = "/media/jellyfin-app-toggle-final.m3u8"
scenario.media_title = ""
fire_event(recorded, "start-file")
fire_event(recorded, "file-loaded")
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
"app-side hide sync should suppress path-changing Jellyfin redirect visible overlay reassertion"
)
assert_true(
count_property_set(recorded.property_sets, "pause", false) == 0,
"app-side hide sync followed by Jellyfin redirect should keep paused playback paused"
)
end
do
local 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",
path = "/media/jellyfin-duplicate-toggle.m3u8",
media_title = "Jellyfin Duplicate Toggle",
paused = true,
now = 10,
files = {
[binary_path] = true,
},
}
local recorded, err = run_plugin_scenario(scenario)
assert_true(recorded ~= nil, "plugin failed to load for duplicate visible overlay toggle: " .. tostring(err))
fire_event(recorded, "file-loaded")
recorded.script_messages["subminer-toggle"]()
recorded.script_messages["subminer-toggle"]()
assert_true(
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 1,
"duplicate same-tick visible overlay toggles should hide once"
)
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
"duplicate same-tick visible overlay toggles should not immediately show the overlay again"
)
scenario.now = 10.5
recorded.script_messages["subminer-toggle"]()
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
"later visible overlay toggle should still show after duplicate suppression window"
)
end
do
local scenario = {
process_list = "",
option_overrides = {
binary_path = binary_path,
auto_start = "no",
},
now = 20,
files = {
[binary_path] = true,
},
}
local recorded, err = run_plugin_scenario(scenario)
assert_true(recorded ~= nil, "plugin failed to load for visible overlay state sync scenario: " .. tostring(err))
assert_true(
recorded.script_messages["subminer-visible-overlay-hidden"] ~= nil,
"hidden visibility sync message should be registered"
)
assert_true(
recorded.script_messages["subminer-visible-overlay-shown"] ~= nil,
"shown visibility sync message should be registered"
)
recorded.script_messages["subminer-visible-overlay-hidden"]()
recorded.script_messages["subminer-toggle"]()
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
"toggle after app-side hide should explicitly show SubMiner overlay through plugin state"
)
assert_true(
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 0,
"toggle after app-side hide should avoid app-side visible overlay toggle"
)
scenario.now = 20.5
recorded.script_messages["subminer-visible-overlay-shown"]()
recorded.script_messages["subminer-toggle"]()
assert_true(
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 1,
"toggle after app-side show should explicitly hide SubMiner overlay through plugin state"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",
@@ -1717,6 +1838,53 @@ do
)
end
do
local 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",
path = "/media/jellyfin-redirect-initial.m3u8",
media_title = "Jellyfin Redirect",
paused = true,
files = {
[binary_path] = true,
},
}
local recorded, err = run_plugin_scenario(scenario)
assert_true(recorded ~= nil, "plugin failed to load for manual hide path-changing Jellyfin redirect: " .. tostring(err))
fire_event(recorded, "start-file")
fire_event(recorded, "file-loaded")
recorded.script_messages["subminer-autoplay-ready"]()
recorded.script_messages["subminer-toggle"]()
fire_event(recorded, "end-file", { reason = "redirect" })
scenario.path = "/media/jellyfin-redirect-final.m3u8"
scenario.media_title = ""
fire_event(recorded, "start-file")
fire_event(recorded, "file-loaded")
assert_true(
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
"manual toggle-off should suppress path-changing Jellyfin redirect visible overlay reassertion even if media-title drops"
)
assert_true(
count_property_set(recorded.property_sets, "pause", false) == 0,
"manual toggle-off followed by path-changing Jellyfin reload should keep paused playback paused"
)
assert_true(
count_property_set(recorded.property_sets, "sid", "auto") == 2,
"path-changing Jellyfin redirect should rearm primary subtitle selection before mpv loads tracks"
)
assert_true(
count_property_set(recorded.property_sets, "secondary-sid", "auto") == 2,
"path-changing Jellyfin redirect should rearm secondary subtitle selection before mpv loads tracks"
)
end
do
local recorded, err = run_plugin_scenario({
process_list = "",