mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-26 00:55:16 -07:00
fix(jellyfin): show overlay, inject plugin, and fix stats title on playback (#77)
* fix(jellyfin): show overlay, inject plugin, and fix stats title on playb - Show visible overlay automatically during Jellyfin playback so subtitleStyle applies - Inject bundled mpv plugin on auto-launch so keybindings work without overlay focus - Group Jellyfin playback stats under item metadata (jellyfin://host/item/id) instead of stream URLs so episodes merge with matching local titles - Mark ffsubsync unavailable in subsync modal for remote media paths - Drain queued second-instance commands even when onReady throws * fix(overlay): stabilize macOS focus handoff and sidebar Yomitan pause - Keep overlay visible during macOS foreground probe after overlay blur - Hold sidebar hover-pause while a Yomitan lookup popup remains open * fix(jellyfin): fix discovery loop, device identity, tray state, and Disc - Derive device identity from OS hostname; remove legacy configurable client/device fields - Prevent discovery playback from reloading active item, misreporting pause state, and duplicate overlay restores - Restart stale tray discovery sessions without re-login when server drops SubMiner cast target - Sync tray discovery checkbox state on Linux after CLI/startup/remote-session changes - Stop Discord presence falling back to stream URLs; prime title before tokenized stream loads - Fix picker library discovery when log level is above info - Fix config.example.jsonc trailing commas and array formatting * docs(release): trim and consolidate prerelease notes for 0.15.0 - Remove breaking changes section and several redundant bullet points - Consolidate per-platform updater notes into a single entry - Normalize em-dash separators to hyphens in section headers * fix(config): remove trailing commas from config.example.jsonc - Strip trailing commas throughout both config.example.jsonc copies - Reformat inline arrays to multi-line for JSON strictness - Update Jellyfin subtitle preload and playback launch tests and impl * fix(tokenizer): preserve known-word highlight when POS filters suppress - Known-word cache matches now set isKnown=true even for tokens excluded by POS filters - POS exclusion gate suppresses N+1, frequency, and JLPT only; known status is computed before the gate - Jellyfin subtitle preload continues after cleanup failures instead of aborting - Update config docs and option description to document the known-word bypass behavior * fix(jellyfin): send explicit hide/show overlay instead of toggle - Track overlay visibility in plugin state; y-t uses explicit hide/show commands when state is known - Prevent paused Jellyfin playback from resuming on overlay hide - Fix subtitle cache cleanup to only remove dirs after successful cleanup * fix(jellyfin): fix remote progress sync, seek reporting, and startup sto - arm active playback before loadfile with loadedMediaPath: null to suppress premature stop events - force immediate progress report on seek-like position jumps at the mpv time-pos level - send positionTicks and failed=false in reportStopped payload - remove EventName from HTTP timeline payloads (websocket-only field) - add startup grace window to drop stop events before media finishes loading * 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 * Fix CodeRabbit review feedback * fix(jellyfin): subtitle timing, resume progress, and overlay sync - Add per-stream subtitle delay persistence and auto timeline-offset correction - Strip server-selected subtitle stream from mpv load URL; suppress plugin subtitle rearm and auto-start during app-managed preload - Fix resume position lost when mpv resets on stop; use last known position for final progress/stopped reports - Keep Play vs Resume distinct to avoid early seek race on normal play - Fix discovery resume when remote play sends StartPositionTicks=0 despite saved progress - Deduplicate show/hide overlay commands using recorded visibility state - Rewrite docs-site Jellyfin page around cast-to-device UX * test: update lifecycle cleanup assertion * fix: clear aborted playback state, fix overlay passthrough, and guard du - Reset app_managed_playback_pending on lifecycle cleanup to prevent state leak into next item - Record visible overlay action only after command succeeds, not before - Non-native passive overlay now always click-through on re-show (fix isNonNativePassiveOverlay ordering) - Defer activeParsedSubtitleMediaPath assignment until after prefetch completes - Move autoplay gate release into the hide branch of toggleVisibleOverlay - Clear active Jellyfin playback when stopping media that never loaded - Reset managed subtitle delay and delay key when no external tracks are available - Await async removeDir in subtitle cache cleanup - Guard duplicate delete clicks in MediaDetailView and SessionsTab with refs - Escape key in DeleteConfirmDialog now calls stopPropagation and stopImmediatePropagation
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -83,10 +83,16 @@ local process = process_module.create({
|
||||
return true
|
||||
end,
|
||||
},
|
||||
environment = {
|
||||
detect_backend = function()
|
||||
return "x11"
|
||||
end,
|
||||
environment = {
|
||||
detect_backend = function()
|
||||
return "x11"
|
||||
end,
|
||||
is_linux = function()
|
||||
return false
|
||||
end,
|
||||
is_subminer_app_running_async = function(callback)
|
||||
callback(false)
|
||||
end,
|
||||
},
|
||||
options_helper = {
|
||||
coerce_bool = function(value, default_value)
|
||||
@@ -125,4 +131,79 @@ for _, timeout_seconds in ipairs(recorded.timeouts) do
|
||||
end
|
||||
assert_true(retry_timeout_seen, "expected shorter bounded retry timeout")
|
||||
|
||||
do
|
||||
local visibility_state = {
|
||||
binary_path = "/tmp/subminer",
|
||||
overlay_running = true,
|
||||
texthooker_running = false,
|
||||
visible_overlay_requested = false,
|
||||
}
|
||||
local visibility_calls = {}
|
||||
local visibility_mp = {}
|
||||
|
||||
function visibility_mp.command_native_async(command, callback)
|
||||
visibility_calls[#visibility_calls + 1] = command
|
||||
if callback then
|
||||
callback(false, { status = 1, stdout = "", stderr = "failed" }, "failed")
|
||||
end
|
||||
end
|
||||
|
||||
local visibility_process = process_module.create({
|
||||
mp = visibility_mp,
|
||||
opts = {
|
||||
backend = "x11",
|
||||
socket_path = "/tmp/subminer.sock",
|
||||
log_level = "debug",
|
||||
texthooker_enabled = true,
|
||||
texthooker_port = 5174,
|
||||
auto_start_visible_overlay = false,
|
||||
},
|
||||
state = visibility_state,
|
||||
binary = {
|
||||
ensure_binary_available = function()
|
||||
return true
|
||||
end,
|
||||
},
|
||||
environment = {
|
||||
detect_backend = function()
|
||||
return "x11"
|
||||
end,
|
||||
is_linux = function()
|
||||
return false
|
||||
end,
|
||||
is_subminer_app_running_async = function(callback)
|
||||
callback(true)
|
||||
end,
|
||||
},
|
||||
options_helper = {
|
||||
coerce_bool = function(value, default_value)
|
||||
if value == true or value == "yes" or value == "true" then
|
||||
return true
|
||||
end
|
||||
if value == false or value == "no" or value == "false" then
|
||||
return false
|
||||
end
|
||||
return default_value
|
||||
end,
|
||||
},
|
||||
log = {
|
||||
subminer_log = function(_level, _scope, line)
|
||||
recorded.logs[#recorded.logs + 1] = line
|
||||
end,
|
||||
show_osd = function(_) end,
|
||||
normalize_log_level = function(value)
|
||||
return value or "info"
|
||||
end,
|
||||
},
|
||||
})
|
||||
|
||||
visibility_process.run_control_command_async("show-visible-overlay")
|
||||
|
||||
assert_true(#visibility_calls == 1, "expected visible overlay command to run")
|
||||
assert_true(
|
||||
visibility_state.visible_overlay_requested == false,
|
||||
"failed visible-overlay command should not update requested visibility state"
|
||||
)
|
||||
end
|
||||
|
||||
print("plugin process retry regression tests: OK")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -643,6 +645,36 @@ 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/aborted-app-managed.m3u8",
|
||||
media_title = "Aborted App Managed",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
}
|
||||
local recorded, err = run_plugin_scenario(scenario)
|
||||
assert_true(recorded ~= nil, "plugin failed to load for aborted app-managed scenario: " .. tostring(err))
|
||||
recorded.script_messages["subminer-managed-subtitles-loading"]()
|
||||
fire_event(recorded, "end-file", { reason = "error" })
|
||||
scenario.path = "/media/next-normal.mkv"
|
||||
scenario.media_title = "Next Normal"
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 1,
|
||||
"aborted app-managed playback should not leak pending state into the next item"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local scenario = {
|
||||
process_list = "",
|
||||
@@ -683,6 +715,236 @@ 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") == 0,
|
||||
"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") == 0,
|
||||
"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") == 1,
|
||||
"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 = "",
|
||||
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-stream.m3u8",
|
||||
media_title = "Jellyfin Episode",
|
||||
paused = true,
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for y-t hide visible overlay scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
local toggle_binding = nil
|
||||
for _, candidate in ipairs(recorded.key_bindings) do
|
||||
if candidate.name == "subminer-toggle" then
|
||||
toggle_binding = candidate
|
||||
break
|
||||
end
|
||||
end
|
||||
assert_true(toggle_binding ~= nil, "y-t toggle binding should be registered")
|
||||
toggle_binding.fn()
|
||||
fire_event(recorded, "file-loaded")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 1,
|
||||
"y-t should hide the known visible overlay explicitly instead of app-side toggle"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 0,
|
||||
"y-t should avoid app-side toggle when plugin knows the overlay is visible"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"manual y-t hide should suppress duplicate auto-start and ready-time visible overlay reassertion"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", false) == 0,
|
||||
"manual y-t hide should not resume paused Jellyfin playback"
|
||||
)
|
||||
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 = "Jellyfin Managed Playback",
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for managed Jellyfin subtitle preload scenario: " .. tostring(err))
|
||||
assert_true(
|
||||
recorded.script_messages["subminer-managed-subtitles-loading"] ~= nil,
|
||||
"managed subtitle preload script message should be registered"
|
||||
)
|
||||
recorded.script_messages["subminer-managed-subtitles-loading"]()
|
||||
fire_event(recorded, "start-file")
|
||||
fire_event(recorded, "file-loaded")
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "sid", "auto"),
|
||||
"managed Jellyfin preload should not rearm primary subtitle auto-selection before app-selected subtitles load"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "secondary-sid", "auto"),
|
||||
"managed Jellyfin preload should not rearm secondary subtitle auto-selection before app-selected subtitles load"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "sub-auto", "fuzzy"),
|
||||
"managed Jellyfin preload should not re-enable subtitle autoloading before app-selected subtitles load"
|
||||
)
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 0,
|
||||
"managed Jellyfin preload should let the app show the overlay after subtitle preload instead of plugin auto-start"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"managed Jellyfin preload should not reassert the visible overlay during duplicate file-loaded events"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", true) == 0,
|
||||
"managed Jellyfin preload should not arm the plugin pause gate before app-selected subtitles load"
|
||||
)
|
||||
fire_event(recorded, "end-file", { reason = "stop" })
|
||||
fire_event(recorded, "start-file")
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "sid", "auto") == 1,
|
||||
"managed subtitle preload suppression should only apply to one playback lifecycle"
|
||||
)
|
||||
assert_true(
|
||||
count_start_calls(recorded.async_calls) == 1,
|
||||
"plugin auto-start should resume after the managed Jellyfin lifecycle ends"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
@@ -932,8 +1194,8 @@ do
|
||||
recorded.script_messages["subminer-restart"]()
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
|
||||
"manual restart should re-assert visible overlay after launch and readiness even when auto-start visibility is disabled"
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
|
||||
"manual restart should avoid a second visible overlay restore after launch already requested visibility"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1328,8 +1590,8 @@ do
|
||||
"auto-start with visible overlay enabled should not include --hide-visible-overlay on --start"
|
||||
)
|
||||
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"
|
||||
find_control_call(recorded.async_calls, "--show-visible-overlay") == nil,
|
||||
"auto-start with visible overlay enabled should rely on the --start visibility flag instead of a separate --show-visible-overlay command"
|
||||
)
|
||||
assert_true(
|
||||
not has_property_set(recorded.property_sets, "pause", true),
|
||||
@@ -1360,8 +1622,8 @@ do
|
||||
"duplicate file-loaded events should not issue duplicate --start commands while overlay is already running"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
|
||||
"duplicate auto-start should re-assert visible overlay state when overlay is already running"
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"duplicate auto-start should not re-assert visible overlay state when it is already requested"
|
||||
)
|
||||
assert_true(
|
||||
count_osd_message(recorded.osd, "SubMiner: Already running") == 0,
|
||||
@@ -1396,8 +1658,8 @@ do
|
||||
"duplicate pause-until-ready auto-start should not issue duplicate --start commands while overlay is already running"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 4,
|
||||
"duplicate pause-until-ready auto-start should re-assert visible overlay on initial start, ready, and later file load"
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"duplicate pause-until-ready auto-start should not re-assert visible overlay after the start command already requested it"
|
||||
)
|
||||
assert_true(
|
||||
count_osd_message(recorded.osd, "SubMiner: Loading subtitle tokenization...") == 1,
|
||||
@@ -1458,8 +1720,8 @@ do
|
||||
"autoplay-ready should show loaded OSD message"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 2,
|
||||
"autoplay-ready should re-assert visible overlay state"
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"autoplay-ready should not re-assert visible overlay state after the start command already requested it"
|
||||
)
|
||||
assert_true(
|
||||
#recorded.periodic_timers == 1,
|
||||
@@ -1471,6 +1733,33 @@ do
|
||||
)
|
||||
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 duplicate autoplay-ready scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(recorded.script_messages["subminer-autoplay-ready"] ~= nil, "subminer-autoplay-ready script message not registered")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"duplicate autoplay-ready signals should not spawn visible overlay restore commands when start already requested visibility"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local recorded, err = run_plugin_scenario({
|
||||
process_list = "",
|
||||
@@ -1523,14 +1812,22 @@ do
|
||||
assert_true(recorded.script_messages["subminer-toggle"] ~= nil, "subminer-toggle script message not registered")
|
||||
recorded.script_messages["subminer-toggle"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 1,
|
||||
"manual toggle should use explicit visible-overlay toggle command"
|
||||
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 1,
|
||||
"manual toggle-off should hide a known visible overlay explicitly"
|
||||
)
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 0,
|
||||
"manual toggle-off should avoid app-side toggle when plugin knows the overlay is visible"
|
||||
)
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"manual toggle-off before readiness should suppress ready-time visible overlay restore"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", false) == 0,
|
||||
"manual toggle-off before readiness should not resume playback when readiness arrives"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
@@ -1559,11 +1856,127 @@ do
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 1,
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"manual toggle-off should suppress repeated ready-time visible overlay restores for the same session"
|
||||
)
|
||||
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",
|
||||
path = "/media/jellyfin-stream.m3u8",
|
||||
media_title = "Jellyfin Episode",
|
||||
paused = true,
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for manual hide duplicate auto-start scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(recorded.script_messages["subminer-toggle"] ~= nil, "subminer-toggle script message not registered")
|
||||
recorded.script_messages["subminer-toggle"]()
|
||||
fire_event(recorded, "file-loaded")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"manual toggle-off should suppress duplicate auto-start visible overlay reassertion"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", false) == 0,
|
||||
"manual toggle-off followed by duplicate auto-start should keep paused playback paused"
|
||||
)
|
||||
end
|
||||
|
||||
do
|
||||
local media_path = "/media/jellyfin-redirect.m3u8"
|
||||
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",
|
||||
path = media_path,
|
||||
media_title = "Jellyfin Redirect",
|
||||
paused = true,
|
||||
files = {
|
||||
[binary_path] = true,
|
||||
},
|
||||
})
|
||||
assert_true(recorded ~= nil, "plugin failed to load for manual hide same-media reload scenario: " .. tostring(err))
|
||||
fire_event(recorded, "file-loaded")
|
||||
recorded.script_messages["subminer-autoplay-ready"]()
|
||||
recorded.script_messages["subminer-toggle"]()
|
||||
fire_event(recorded, "end-file", { reason = "redirect" })
|
||||
fire_event(recorded, "file-loaded")
|
||||
assert_true(
|
||||
count_control_calls(recorded.async_calls, "--show-visible-overlay") == 0,
|
||||
"manual toggle-off should suppress same-media reload visible overlay reassertion"
|
||||
)
|
||||
assert_true(
|
||||
count_property_set(recorded.property_sets, "pause", false) == 0,
|
||||
"manual toggle-off followed by same-media reload 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-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") == 0,
|
||||
"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 = "",
|
||||
@@ -1721,8 +2134,8 @@ do
|
||||
"auto-start with visible overlay disabled should not include --show-visible-overlay on --start"
|
||||
)
|
||||
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"
|
||||
find_control_call(recorded.async_calls, "--hide-visible-overlay") == nil,
|
||||
"auto-start with visible overlay disabled should rely on the --start visibility flag instead of a separate --hide-visible-overlay command"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user