fix: address playlist browser coderabbit feedback

This commit is contained in:
2026-03-30 17:50:39 -07:00
parent 6ae3888b53
commit f901433eea
11 changed files with 494 additions and 31 deletions

View File

@@ -34,6 +34,17 @@ function M.create(ctx)
return options_helper.coerce_bool(raw_pause_until_ready, false)
end
local function resolve_texthooker_enabled(override_value)
if override_value ~= nil then
return options_helper.coerce_bool(override_value, false)
end
local raw_texthooker_enabled = opts.texthooker_enabled
if raw_texthooker_enabled == nil then
raw_texthooker_enabled = opts["texthooker-enabled"]
end
return options_helper.coerce_bool(raw_texthooker_enabled, false)
end
local function resolve_pause_until_ready_timeout_seconds()
local raw_timeout_seconds = opts.auto_start_pause_until_ready_timeout_seconds
if raw_timeout_seconds == nil then
@@ -192,10 +203,7 @@ function M.create(ctx)
table.insert(args, "--hide-visible-overlay")
end
local texthooker_enabled = overrides.texthooker_enabled
if texthooker_enabled == nil then
texthooker_enabled = opts.texthooker_enabled
end
local texthooker_enabled = resolve_texthooker_enabled(overrides.texthooker_enabled)
if texthooker_enabled then
table.insert(args, "--texthooker")
end
@@ -296,10 +304,7 @@ function M.create(ctx)
return
end
local texthooker_enabled = overrides.texthooker_enabled
if texthooker_enabled == nil then
texthooker_enabled = opts.texthooker_enabled
end
local texthooker_enabled = resolve_texthooker_enabled(overrides.texthooker_enabled)
local socket_path = overrides.socket_path or opts.socket_path
local should_pause_until_ready = (
overrides.auto_start_trigger == true
@@ -498,7 +503,7 @@ function M.create(ctx)
end
end)
if opts.texthooker_enabled then
if resolve_texthooker_enabled(nil) then
ensure_texthooker_running(function() end)
end
end)

View File

@@ -531,6 +531,31 @@ 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 = "no",
socket_path = "/tmp/subminer-socket",
texthooker_enabled = "no",
},
input_ipc_server = "/tmp/subminer-socket",
media_title = "Random Movie",
files = {
[binary_path] = true,
},
})
assert_true(recorded ~= nil, "plugin failed to load for disabled texthooker auto-start scenario: " .. tostring(err))
fire_event(recorded, "file-loaded")
local start_call = find_start_call(recorded.async_calls)
assert_true(start_call ~= nil, "disabled texthooker auto-start should still issue --start command")
assert_true(not call_has_arg(start_call, "--texthooker"), "disabled texthooker should not include --texthooker on --start")
assert_true(find_control_call(recorded.async_calls, "--texthooker") == nil, "disabled texthooker should not issue a helper texthooker command")
end
do
local recorded, err = run_plugin_scenario({
process_list = "",

View File

@@ -114,14 +114,28 @@ test('handleMpvCommandFromIpc dispatches special youtube picker open command', (
assert.deepEqual(osd, []);
});
test('handleMpvCommandFromIpc dispatches special playlist browser open command', () => {
test('handleMpvCommandFromIpc dispatches special playlist browser open command', async () => {
const { options, calls, sentCommands, osd } = createOptions();
handleMpvCommandFromIpc(['__playlist-browser-open'], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(calls, ['playlist-browser']);
assert.deepEqual(sentCommands, []);
assert.deepEqual(osd, []);
});
test('handleMpvCommandFromIpc surfaces playlist browser open rejections via mpv osd', async () => {
const { options, osd } = createOptions({
openPlaylistBrowser: async () => {
throw new Error('overlay failed');
},
});
handleMpvCommandFromIpc(['__playlist-browser-open'], options);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(osd, ['Playlist browser failed: overlay failed']);
});
test('handleMpvCommandFromIpc does not forward commands while disconnected', () => {
const { options, sentCommands, osd } = createOptions({
isMpvConnected: () => false,

View File

@@ -100,7 +100,12 @@ export function handleMpvCommandFromIpc(
}
if (first === options.specialCommands.PLAYLIST_BROWSER_OPEN) {
void options.openPlaylistBrowser();
Promise.resolve()
.then(() => options.openPlaylistBrowser())
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
options.showMpvOsd(`Playlist browser failed: ${message}`);
});
return;
}

View File

@@ -31,6 +31,7 @@ import {
screen,
} from 'electron';
import { applyControllerConfigUpdate } from './main/controller-config-update.js';
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
import { createDiscordRpcClient } from './main/runtime/discord-rpc-client.js';
import { mergeAiConfig } from './ai/config';
@@ -1941,8 +1942,12 @@ function openPlaylistBrowser(): void {
showMpvOsd('Playlist browser requires active playback.');
return;
}
const opened = sendToActiveOverlayWindow(IPC_CHANNELS.event.playlistBrowserOpen, undefined, {
restoreOnModalClose: 'playlist-browser',
const opened = openPlaylistBrowserRuntime({
ensureOverlayStartupPrereqs: () => ensureOverlayStartupPrereqs(),
ensureOverlayWindowsReadyForVisibilityActions: () =>
ensureOverlayWindowsReadyForVisibilityActions(),
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
sendToActiveOverlayWindow(channel, payload, runtimeOptions),
});
if (!opened) {
showMpvOsd('Playlist browser overlay unavailable.');

View File

@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
import { openPlaylistBrowser } from './playlist-browser-open';
test('playlist browser open bootstraps overlay runtime before dispatching the modal event', () => {
const calls: string[] = [];
const opened = openPlaylistBrowser({
ensureOverlayStartupPrereqs: () => {
calls.push('prereqs');
},
ensureOverlayWindowsReadyForVisibilityActions: () => {
calls.push('windows');
},
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) => {
calls.push(`send:${channel}`);
assert.equal(payload, undefined);
assert.deepEqual(runtimeOptions, {
restoreOnModalClose: 'playlist-browser',
});
return true;
},
});
assert.equal(opened, true);
assert.deepEqual(calls, ['prereqs', 'windows', `send:${IPC_CHANNELS.event.playlistBrowserOpen}`]);
});

View File

@@ -0,0 +1,23 @@
import type { OverlayHostedModal } from '../../shared/ipc/contracts';
import { IPC_CHANNELS } from '../../shared/ipc/contracts';
const PLAYLIST_BROWSER_MODAL: OverlayHostedModal = 'playlist-browser';
export function openPlaylistBrowser(deps: {
ensureOverlayStartupPrereqs: () => void;
ensureOverlayWindowsReadyForVisibilityActions: () => void;
sendToActiveOverlayWindow: (
channel: string,
payload?: unknown,
runtimeOptions?: {
restoreOnModalClose?: OverlayHostedModal;
preferModalWindow?: boolean;
},
) => boolean;
}): boolean {
deps.ensureOverlayStartupPrereqs();
deps.ensureOverlayWindowsReadyForVisibilityActions();
return deps.sendToActiveOverlayWindow(IPC_CHANNELS.event.playlistBrowserOpen, undefined, {
restoreOnModalClose: PLAYLIST_BROWSER_MODAL,
});
}

View File

@@ -249,6 +249,41 @@ test('playlist-browser mutation runtimes mutate queue and return refreshed snaps
);
});
test('appendPlaylistBrowserFileRuntime returns an error result when statSync throws', async () => {
const dir = createTempVideoDir();
const episode1 = path.join(dir, 'Show - S01E01.mkv');
fs.writeFileSync(episode1, '');
const mutableFs = fs as typeof fs & { statSync: typeof fs.statSync };
const originalStatSync = mutableFs.statSync;
mutableFs.statSync = ((targetPath: fs.PathLike) => {
if (path.resolve(String(targetPath)) === episode1) {
throw new Error('EACCES');
}
return originalStatSync(targetPath);
}) as typeof fs.statSync;
try {
const result = await appendPlaylistBrowserFileRuntime(
{
getMpvClient: () =>
createFakeMpvClient({
currentVideoPath: episode1,
playlist: [{ filename: episode1, current: true }],
}),
},
episode1,
);
assert.deepEqual(result, {
ok: false,
message: 'Playlist browser file is not readable.',
});
} finally {
mutableFs.statSync = originalStatSync;
}
});
test('movePlaylistBrowserIndexRuntime rejects top and bottom boundary moves', async () => {
const dir = createTempVideoDir();
const episode1 = path.join(dir, 'Show - S01E01.mkv');
@@ -324,3 +359,52 @@ test('playPlaylistBrowserIndexRuntime skips local subtitle reset for remote play
assert.deepEqual(mpvClient.getCommands().slice(-1), [['playlist-play-index', 1]]);
assert.equal(scheduled.length, 0);
});
test('playPlaylistBrowserIndexRuntime ignores superseded local subtitle rearm callbacks', async () => {
const dir = createTempVideoDir();
const episode1 = path.join(dir, 'Show - S01E01.mkv');
const episode2 = path.join(dir, 'Show - S01E02.mkv');
const episode3 = path.join(dir, 'Show - S01E03.mkv');
fs.writeFileSync(episode1, '');
fs.writeFileSync(episode2, '');
fs.writeFileSync(episode3, '');
const scheduled: Array<() => void> = [];
const mpvClient = createFakeMpvClient({
currentVideoPath: episode1,
playlist: [
{ filename: episode1, current: true, title: 'Episode 1' },
{ filename: episode2, title: 'Episode 2' },
{ filename: episode3, title: 'Episode 3' },
],
});
const deps = {
getMpvClient: () => mpvClient,
schedule: (callback: () => void) => {
scheduled.push(callback);
},
};
const firstPlay = await playPlaylistBrowserIndexRuntime(deps, 1);
const secondPlay = await playPlaylistBrowserIndexRuntime(deps, 2);
assert.equal(firstPlay.ok, true);
assert.equal(secondPlay.ok, true);
assert.equal(scheduled.length, 2);
scheduled[0]?.();
scheduled[1]?.();
assert.deepEqual(
mpvClient.getCommands().slice(-6),
[
['set_property', 'sub-auto', 'fuzzy'],
['playlist-play-index', 1],
['set_property', 'sub-auto', 'fuzzy'],
['playlist-play-index', 2],
['set_property', 'sid', 'auto'],
['set_property', 'secondary-sid', 'auto'],
],
);
});

View File

@@ -30,6 +30,8 @@ export type PlaylistBrowserRuntimeDeps = {
schedule?: (callback: () => void, delayMs: number) => void;
};
const pendingLocalSubtitleSelectionRearms = new WeakMap<MpvPlaylistBrowserClientLike, number>();
function trimToNull(value: unknown): string | null {
if (typeof value !== 'string') return null;
const trimmed = value.trim();
@@ -219,15 +221,26 @@ function prepareLocalSubtitleAutoload(client: MpvPlaylistBrowserClientLike): voi
client.send({ command: ['set_property', 'sub-auto', 'fuzzy'] });
}
function isLocalPlaylistItem(item: PlaylistBrowserQueueItem | null | undefined): item is PlaylistBrowserQueueItem {
function isLocalPlaylistItem(
item: PlaylistBrowserQueueItem | null | undefined,
): item is PlaylistBrowserQueueItem & { path: string } {
return Boolean(item?.path && !isRemoteMediaPath(item.path));
}
function scheduleLocalSubtitleSelectionRearm(
deps: PlaylistBrowserRuntimeDeps,
client: MpvPlaylistBrowserClientLike,
expectedPath: string,
): void {
const nextToken = (pendingLocalSubtitleSelectionRearms.get(client) ?? 0) + 1;
pendingLocalSubtitleSelectionRearms.set(client, nextToken);
(deps.schedule ?? setTimeout)(() => {
if (pendingLocalSubtitleSelectionRearms.get(client) !== nextToken) return;
pendingLocalSubtitleSelectionRearms.delete(client);
const currentPath = trimToNull(client.currentVideoPath);
if (currentPath && path.resolve(currentPath) !== expectedPath) {
return;
}
rearmLocalSubtitleSelection(client);
}, 400);
}
@@ -241,7 +254,16 @@ export async function appendPlaylistBrowserFileRuntime(
return client;
}
const resolvedPath = path.resolve(filePath);
if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isFile()) {
let stats: fs.Stats;
try {
stats = fs.statSync(resolvedPath);
} catch {
return {
ok: false,
message: 'Playlist browser file is not readable.',
};
}
if (!stats.isFile()) {
return {
ok: false,
message: 'Playlist browser file is not readable.',
@@ -267,7 +289,7 @@ export async function playPlaylistBrowserIndexRuntime(
}
result.client.send({ command: ['playlist-play-index', index] });
if (isLocalPlaylistItem(targetItem)) {
scheduleLocalSubtitleSelectionRearm(deps, result.client);
scheduleLocalSubtitleSelectionRearm(deps, result.client, path.resolve(targetItem.path));
}
return buildMutationResult(`Playing playlist item ${index + 1}`, deps);
}

View File

@@ -428,3 +428,241 @@ test('playlist browser keeps modal open when playing selected queue item fails',
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}
});
test('playlist browser refresh failure clears stale rendered rows and reports the error', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = [];
let refreshShouldFail = false;
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
electronAPI: {
getPlaylistBrowserSnapshot: async () => {
if (refreshShouldFail) {
throw new Error('snapshot failed');
}
return createSnapshot();
},
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
},
});
try {
const state = createRendererState();
const playlistBrowserTitle = createFakeElement();
const playlistBrowserStatus = createFakeElement();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle,
playlistBrowserStatus,
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal();
assert.equal(directoryList.children.length, 2);
assert.equal(playlistList.children.length, 2);
refreshShouldFail = true;
await modal.refreshSnapshot();
assert.equal(state.playlistBrowserSnapshot, null);
assert.equal(directoryList.children.length, 0);
assert.equal(playlistList.children.length, 0);
assert.equal(playlistBrowserTitle.textContent, 'Playlist Browser');
assert.equal(playlistBrowserStatus.textContent, 'snapshot failed');
assert.equal(playlistBrowserStatus.classList.contains('error'), true);
assert.deepEqual(notifications, ['open:playlist-browser']);
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}
});
test('playlist browser close clears rendered snapshot ui', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = [];
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
electronAPI: {
getPlaylistBrowserSnapshot: async () => createSnapshot(),
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
},
});
try {
const state = createRendererState();
const playlistBrowserTitle = createFakeElement();
const playlistBrowserStatus = createFakeElement();
const directoryList = createListStub();
const playlistList = createListStub();
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay: {
classList: createClassList(),
focus: () => {},
},
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle,
playlistBrowserStatus,
playlistBrowserDirectoryList: directoryList,
playlistBrowserPlaylistList: playlistList,
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => false },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal();
assert.equal(directoryList.children.length, 2);
assert.equal(playlistList.children.length, 2);
modal.closePlaylistBrowserModal();
assert.equal(state.playlistBrowserSnapshot, null);
assert.equal(state.playlistBrowserStatus, '');
assert.equal(directoryList.children.length, 0);
assert.equal(playlistList.children.length, 0);
assert.equal(playlistBrowserTitle.textContent, 'Playlist Browser');
assert.equal(playlistBrowserStatus.textContent, '');
assert.deepEqual(notifications, ['open:playlist-browser', 'close:playlist-browser']);
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}
});
test('playlist browser open is ignored while another modal is already open', async () => {
const globals = globalThis as typeof globalThis & { window?: unknown; document?: unknown };
const previousWindow = globals.window;
const previousDocument = globals.document;
const notifications: string[] = [];
let snapshotCalls = 0;
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
electronAPI: {
getPlaylistBrowserSnapshot: async () => {
snapshotCalls += 1;
return createSnapshot();
},
notifyOverlayModalOpened: (modal: string) => notifications.push(`open:${modal}`),
notifyOverlayModalClosed: (modal: string) => notifications.push(`close:${modal}`),
focusMainWindow: async () => {},
setIgnoreMouseEvents: () => {},
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok', snapshot: createSnapshot() }),
} as unknown as ElectronAPI,
focus: () => {},
},
});
Object.defineProperty(globalThis, 'document', {
configurable: true,
value: {
createElement: () => createPlaylistRow(),
},
});
try {
const state = createRendererState();
const overlay = {
classList: createClassList(),
focus: () => {},
};
const ctx = {
state,
platform: {
shouldToggleMouseIgnore: false,
},
dom: {
overlay,
playlistBrowserModal: createFakeElement(),
playlistBrowserTitle: createFakeElement(),
playlistBrowserStatus: createFakeElement(),
playlistBrowserDirectoryList: createListStub(),
playlistBrowserPlaylistList: createListStub(),
playlistBrowserClose: createFakeElement(),
},
};
const modal = createPlaylistBrowserModal(ctx as never, {
modalStateReader: { isAnyModalOpen: () => true },
syncSettingsModalSubtitleSuppression: () => {},
});
await modal.openPlaylistBrowserModal();
assert.equal(state.playlistBrowserModalOpen, false);
assert.equal(snapshotCalls, 0);
assert.equal(overlay.classList.contains('interactive'), false);
assert.deepEqual(notifications, []);
} finally {
Object.defineProperty(globalThis, 'window', { configurable: true, value: previousWindow });
Object.defineProperty(globalThis, 'document', { configurable: true, value: previousDocument });
}
});

View File

@@ -49,6 +49,18 @@ export function createPlaylistBrowserModal(
return ctx.state.playlistBrowserSnapshot;
}
function resetSnapshotUi(): void {
ctx.state.playlistBrowserSnapshot = null;
ctx.state.playlistBrowserStatus = '';
ctx.state.playlistBrowserSelectedDirectoryIndex = 0;
ctx.state.playlistBrowserSelectedPlaylistIndex = 0;
ctx.dom.playlistBrowserTitle.textContent = 'Playlist Browser';
ctx.dom.playlistBrowserDirectoryList.replaceChildren();
ctx.dom.playlistBrowserPlaylistList.replaceChildren();
ctx.dom.playlistBrowserStatus.textContent = '';
ctx.dom.playlistBrowserStatus.classList.remove('error');
}
function syncSelection(snapshot: PlaylistBrowserSnapshot): void {
const directoryIndex = snapshot.directoryItems.findIndex((item) => item.isCurrentFile);
const playlistIndex =
@@ -194,6 +206,7 @@ export function createPlaylistBrowserModal(
}
async function refreshSnapshot(): Promise<void> {
try {
const snapshot = await window.electronAPI.getPlaylistBrowserSnapshot();
ctx.state.playlistBrowserStatus = '';
applySnapshot(snapshot);
@@ -201,6 +214,10 @@ export function createPlaylistBrowserModal(
buildDefaultStatus(snapshot),
!snapshot.directoryAvailable && snapshot.directoryStatus.length > 0,
);
} catch (error) {
resetSnapshotUi();
setStatus(error instanceof Error ? error.message : String(error), true);
}
}
async function handleMutation(
@@ -249,6 +266,9 @@ export function createPlaylistBrowserModal(
await refreshSnapshot();
return;
}
if (options.modalStateReader.isAnyModalOpen()) {
return;
}
ctx.state.playlistBrowserModalOpen = true;
ctx.state.playlistBrowserActivePane = 'playlist';
@@ -257,19 +277,13 @@ export function createPlaylistBrowserModal(
ctx.dom.playlistBrowserModal.classList.remove('hidden');
ctx.dom.playlistBrowserModal.setAttribute('aria-hidden', 'false');
window.electronAPI.notifyOverlayModalOpened('playlist-browser');
try {
await refreshSnapshot();
} catch (error) {
setStatus(error instanceof Error ? error.message : String(error), true);
}
}
function closePlaylistBrowserModal(): void {
if (!ctx.state.playlistBrowserModalOpen) return;
ctx.state.playlistBrowserModalOpen = false;
ctx.state.playlistBrowserSnapshot = null;
ctx.state.playlistBrowserStatus = '';
resetSnapshotUi();
ctx.dom.playlistBrowserModal.classList.add('hidden');
ctx.dom.playlistBrowserModal.setAttribute('aria-hidden', 'true');
window.electronAPI.notifyOverlayModalClosed('playlist-browser');