diff --git a/changes/fix-jellyfin-overlay-toggle.md b/changes/fix-jellyfin-overlay-toggle.md new file mode 100644 index 00000000..785cb4e3 --- /dev/null +++ b/changes/fix-jellyfin-overlay-toggle.md @@ -0,0 +1,4 @@ +type: fixed +area: jellyfin + +- Fixed Jellyfin `y-t` overlay hide so the plugin sends an explicit hide command when it knows the overlay is visible, avoiding overlay reloads and paused playback resumes. diff --git a/plugin/subminer/process.lua b/plugin/subminer/process.lua index 6afff0ce..18a8dd61 100644 --- a/plugin/subminer/process.lua +++ b/plugin/subminer/process.lua @@ -77,6 +77,20 @@ function M.create(ctx) return DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS end + local function record_visible_overlay_action(action) + if action == "show-visible-overlay" then + state.visible_overlay_requested = true + state.suppress_ready_overlay_restore = false + elseif action == "hide-visible-overlay" then + state.visible_overlay_requested = false + elseif action == "toggle-visible-overlay" and state.visible_overlay_requested ~= nil then + state.visible_overlay_requested = not state.visible_overlay_requested + if state.visible_overlay_requested then + state.suppress_ready_overlay_restore = false + end + end + end + local function normalize_socket_path(path) if type(path) ~= "string" then return nil @@ -317,6 +331,7 @@ function M.create(ctx) end run_control_command_async = function(action, overrides, callback) + record_visible_overlay_action(action) local args = build_command_args(action, overrides) local command = build_subprocess_command(args) subminer_log("debug", "process", "Control command: " .. table.concat(args, " ")) @@ -557,7 +572,8 @@ function M.create(ctx) show_osd("Stopped") end - local function hide_visible_overlay() + local function hide_visible_overlay(options) + options = options or {} if not binary.ensure_binary_available() then subminer_log("error", "binary", "SubMiner binary not found") return @@ -577,7 +593,9 @@ function M.create(ctx) end end) - disarm_auto_play_ready_gate() + disarm_auto_play_ready_gate({ + resume_playback = options.resume_playback ~= false, + }) end local function toggle_overlay() @@ -586,6 +604,22 @@ function M.create(ctx) show_osd("Error: binary not found") return end + if state.visible_overlay_requested == true then + state.suppress_ready_overlay_restore = true + hide_visible_overlay({ resume_playback = false }) + return + end + if state.visible_overlay_requested == false then + state.suppress_ready_overlay_restore = false + disarm_auto_play_ready_gate({ resume_playback = false }) + run_control_command_async("show-visible-overlay", nil, function(ok) + if not ok then + subminer_log("warn", "process", "Show-visible-overlay command failed") + show_osd("Toggle failed") + end + end) + return + end state.suppress_ready_overlay_restore = true disarm_auto_play_ready_gate({ resume_playback = false }) diff --git a/plugin/subminer/state.lua b/plugin/subminer/state.lua index 7444e5ad..fd3b9d50 100644 --- a/plugin/subminer/state.lua +++ b/plugin/subminer/state.lua @@ -35,6 +35,7 @@ function M.new() auto_play_ready_osd_timer = nil, suppress_ready_overlay_restore = false, force_ready_overlay_restore = false, + visible_overlay_requested = nil, current_media_identity = nil, pending_reload_media_identity = nil, auto_start_retry_generation = 0, diff --git a/scripts/test-plugin-start-gate.lua b/scripts/test-plugin-start-gate.lua index d0b70b51..181d9d09 100644 --- a/scripts/test-plugin-start-gate.lua +++ b/scripts/test-plugin-start-gate.lua @@ -683,6 +683,55 @@ 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", + 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") == 1, + "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 = "", @@ -1550,8 +1599,12 @@ 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( diff --git a/src/main/runtime/jellyfin-subtitle-preload.test.ts b/src/main/runtime/jellyfin-subtitle-preload.test.ts index a0535778..df3e8f00 100644 --- a/src/main/runtime/jellyfin-subtitle-preload.test.ts +++ b/src/main/runtime/jellyfin-subtitle-preload.test.ts @@ -333,12 +333,18 @@ test('preload jellyfin subtitles cleans previous cached subtitles before a new p test('preload jellyfin subtitles continues after cleanup failures', async () => { const commands: Array> = []; + const cleanupCalls: string[][] = []; const logs: string[] = []; let cleanupShouldFail = false; const preload = createPreloadJellyfinExternalSubtitlesHandler( makeDeps({ - listJellyfinSubtitleTracks: async () => [ - { index: 0, language: 'eng', title: 'English', deliveryUrl: 'https://sub/a.srt' }, + listJellyfinSubtitleTracks: async (_session, _clientInfo, itemId) => [ + { + index: itemId === 'item-1' ? 0 : 1, + language: 'eng', + title: 'English', + deliveryUrl: `https://sub/${itemId}.srt`, + }, ], getMpvClient: () => ({ requestProperty: async () => [] }), cacheSubtitleTrack: async (track) => ({ @@ -346,7 +352,8 @@ test('preload jellyfin subtitles continues after cleanup failures', async () => cleanupDir: `/tmp/subminer-jellyfin-subtitles-${track.index}`, }), sendMpvCommand: (command) => commands.push(command), - cleanupCachedSubtitles: () => { + cleanupCachedSubtitles: (dirs) => { + cleanupCalls.push(dirs); if (cleanupShouldFail) { throw new Error('cleanup failed'); } @@ -358,13 +365,19 @@ test('preload jellyfin subtitles continues after cleanup failures', async () => await preload({ session, clientInfo, itemId: 'item-1' }); cleanupShouldFail = true; await assert.doesNotReject(() => preload({ session, clientInfo, itemId: 'item-2' })); + cleanupShouldFail = false; + preload.cleanupCachedSubtitles(); assert.deepEqual(logs, ['Failed to cleanup Jellyfin cached subtitles']); + assert.deepEqual(cleanupCalls, [ + ['/tmp/subminer-jellyfin-subtitles-0'], + ['/tmp/subminer-jellyfin-subtitles-0', '/tmp/subminer-jellyfin-subtitles-1'], + ]); assert.deepEqual( commands.filter((command) => command[0] === 'sub-add'), [ ['sub-add', '/tmp/subminer-jellyfin-subtitles-0/track.srt', 'auto', 'English', 'eng'], - ['sub-add', '/tmp/subminer-jellyfin-subtitles-0/track.srt', 'auto', 'English', 'eng'], + ['sub-add', '/tmp/subminer-jellyfin-subtitles-1/track.srt', 'auto', 'English', 'eng'], ], ); }); diff --git a/src/main/runtime/jellyfin-subtitle-preload.ts b/src/main/runtime/jellyfin-subtitle-preload.ts index c8cd9586..933cbec7 100644 --- a/src/main/runtime/jellyfin-subtitle-preload.ts +++ b/src/main/runtime/jellyfin-subtitle-preload.ts @@ -235,9 +235,11 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: { function cleanupActiveCache(): void { const dirs = [...activeCacheDirs]; - activeCacheDirs.clear(); if (dirs.length === 0) return; deps.cleanupCachedSubtitles(dirs); + for (const dir of dirs) { + activeCacheDirs.delete(dir); + } } const runPreload = async (params: {