mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-05-25 12:55:18 -07:00
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
This commit is contained in:
@@ -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.
|
||||||
@@ -77,6 +77,20 @@ function M.create(ctx)
|
|||||||
return DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS
|
return DEFAULT_AUTO_PLAY_READY_TIMEOUT_SECONDS
|
||||||
end
|
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)
|
local function normalize_socket_path(path)
|
||||||
if type(path) ~= "string" then
|
if type(path) ~= "string" then
|
||||||
return nil
|
return nil
|
||||||
@@ -317,6 +331,7 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
|
|
||||||
run_control_command_async = function(action, overrides, callback)
|
run_control_command_async = function(action, overrides, callback)
|
||||||
|
record_visible_overlay_action(action)
|
||||||
local args = build_command_args(action, overrides)
|
local args = build_command_args(action, overrides)
|
||||||
local command = build_subprocess_command(args)
|
local command = build_subprocess_command(args)
|
||||||
subminer_log("debug", "process", "Control command: " .. table.concat(args, " "))
|
subminer_log("debug", "process", "Control command: " .. table.concat(args, " "))
|
||||||
@@ -557,7 +572,8 @@ function M.create(ctx)
|
|||||||
show_osd("Stopped")
|
show_osd("Stopped")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function hide_visible_overlay()
|
local function hide_visible_overlay(options)
|
||||||
|
options = options or {}
|
||||||
if not binary.ensure_binary_available() then
|
if not binary.ensure_binary_available() then
|
||||||
subminer_log("error", "binary", "SubMiner binary not found")
|
subminer_log("error", "binary", "SubMiner binary not found")
|
||||||
return
|
return
|
||||||
@@ -577,7 +593,9 @@ function M.create(ctx)
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
disarm_auto_play_ready_gate()
|
disarm_auto_play_ready_gate({
|
||||||
|
resume_playback = options.resume_playback ~= false,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function toggle_overlay()
|
local function toggle_overlay()
|
||||||
@@ -586,6 +604,22 @@ function M.create(ctx)
|
|||||||
show_osd("Error: binary not found")
|
show_osd("Error: binary not found")
|
||||||
return
|
return
|
||||||
end
|
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
|
state.suppress_ready_overlay_restore = true
|
||||||
disarm_auto_play_ready_gate({ resume_playback = false })
|
disarm_auto_play_ready_gate({ resume_playback = false })
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function M.new()
|
|||||||
auto_play_ready_osd_timer = nil,
|
auto_play_ready_osd_timer = nil,
|
||||||
suppress_ready_overlay_restore = false,
|
suppress_ready_overlay_restore = false,
|
||||||
force_ready_overlay_restore = false,
|
force_ready_overlay_restore = false,
|
||||||
|
visible_overlay_requested = nil,
|
||||||
current_media_identity = nil,
|
current_media_identity = nil,
|
||||||
pending_reload_media_identity = nil,
|
pending_reload_media_identity = nil,
|
||||||
auto_start_retry_generation = 0,
|
auto_start_retry_generation = 0,
|
||||||
|
|||||||
@@ -683,6 +683,55 @@ do
|
|||||||
)
|
)
|
||||||
end
|
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
|
do
|
||||||
local recorded, err = run_plugin_scenario({
|
local recorded, err = run_plugin_scenario({
|
||||||
process_list = "",
|
process_list = "",
|
||||||
@@ -1550,8 +1599,12 @@ do
|
|||||||
assert_true(recorded.script_messages["subminer-toggle"] ~= nil, "subminer-toggle script message not registered")
|
assert_true(recorded.script_messages["subminer-toggle"] ~= nil, "subminer-toggle script message not registered")
|
||||||
recorded.script_messages["subminer-toggle"]()
|
recorded.script_messages["subminer-toggle"]()
|
||||||
assert_true(
|
assert_true(
|
||||||
count_control_calls(recorded.async_calls, "--toggle-visible-overlay") == 1,
|
count_control_calls(recorded.async_calls, "--hide-visible-overlay") == 1,
|
||||||
"manual toggle should use explicit visible-overlay toggle command"
|
"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"]()
|
recorded.script_messages["subminer-autoplay-ready"]()
|
||||||
assert_true(
|
assert_true(
|
||||||
|
|||||||
@@ -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 () => {
|
test('preload jellyfin subtitles continues after cleanup failures', async () => {
|
||||||
const commands: Array<Array<string | number>> = [];
|
const commands: Array<Array<string | number>> = [];
|
||||||
|
const cleanupCalls: string[][] = [];
|
||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
let cleanupShouldFail = false;
|
let cleanupShouldFail = false;
|
||||||
const preload = createPreloadJellyfinExternalSubtitlesHandler(
|
const preload = createPreloadJellyfinExternalSubtitlesHandler(
|
||||||
makeDeps({
|
makeDeps({
|
||||||
listJellyfinSubtitleTracks: async () => [
|
listJellyfinSubtitleTracks: async (_session, _clientInfo, itemId) => [
|
||||||
{ index: 0, language: 'eng', title: 'English', deliveryUrl: 'https://sub/a.srt' },
|
{
|
||||||
|
index: itemId === 'item-1' ? 0 : 1,
|
||||||
|
language: 'eng',
|
||||||
|
title: 'English',
|
||||||
|
deliveryUrl: `https://sub/${itemId}.srt`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
getMpvClient: () => ({ requestProperty: async () => [] }),
|
getMpvClient: () => ({ requestProperty: async () => [] }),
|
||||||
cacheSubtitleTrack: async (track) => ({
|
cacheSubtitleTrack: async (track) => ({
|
||||||
@@ -346,7 +352,8 @@ test('preload jellyfin subtitles continues after cleanup failures', async () =>
|
|||||||
cleanupDir: `/tmp/subminer-jellyfin-subtitles-${track.index}`,
|
cleanupDir: `/tmp/subminer-jellyfin-subtitles-${track.index}`,
|
||||||
}),
|
}),
|
||||||
sendMpvCommand: (command) => commands.push(command),
|
sendMpvCommand: (command) => commands.push(command),
|
||||||
cleanupCachedSubtitles: () => {
|
cleanupCachedSubtitles: (dirs) => {
|
||||||
|
cleanupCalls.push(dirs);
|
||||||
if (cleanupShouldFail) {
|
if (cleanupShouldFail) {
|
||||||
throw new Error('cleanup failed');
|
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' });
|
await preload({ session, clientInfo, itemId: 'item-1' });
|
||||||
cleanupShouldFail = true;
|
cleanupShouldFail = true;
|
||||||
await assert.doesNotReject(() => preload({ session, clientInfo, itemId: 'item-2' }));
|
await assert.doesNotReject(() => preload({ session, clientInfo, itemId: 'item-2' }));
|
||||||
|
cleanupShouldFail = false;
|
||||||
|
preload.cleanupCachedSubtitles();
|
||||||
|
|
||||||
assert.deepEqual(logs, ['Failed to cleanup Jellyfin cached subtitles']);
|
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(
|
assert.deepEqual(
|
||||||
commands.filter((command) => command[0] === 'sub-add'),
|
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-0/track.srt', 'auto', 'English', 'eng'],
|
['sub-add', '/tmp/subminer-jellyfin-subtitles-1/track.srt', 'auto', 'English', 'eng'],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -235,9 +235,11 @@ export function createPreloadJellyfinExternalSubtitlesHandler(deps: {
|
|||||||
|
|
||||||
function cleanupActiveCache(): void {
|
function cleanupActiveCache(): void {
|
||||||
const dirs = [...activeCacheDirs];
|
const dirs = [...activeCacheDirs];
|
||||||
activeCacheDirs.clear();
|
|
||||||
if (dirs.length === 0) return;
|
if (dirs.length === 0) return;
|
||||||
deps.cleanupCachedSubtitles(dirs);
|
deps.cleanupCachedSubtitles(dirs);
|
||||||
|
for (const dir of dirs) {
|
||||||
|
activeCacheDirs.delete(dir);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const runPreload = async (params: {
|
const runPreload = async (params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user