Merge pull request #2 from PBandDev/flatten-sounds

refactor: flatten sound storage
This commit is contained in:
PBandDev
2026-03-20 15:33:57 -05:00
committed by GitHub
25 changed files with 293 additions and 452 deletions
+5 -3
View File
@@ -31,7 +31,7 @@ From there you can:
- turn notifications on or off
- choose success and failure sounds
- preview sounds
- upload a custom sound
- upload a sound
- adjust volume
- choose whether notifications fire on every prompt or only when the queue finishes
@@ -51,7 +51,7 @@ Defaults:
- success: `ping-success.wav`
- failure: `ping-failure.wav`
## Custom sounds
## Uploads
Supported formats:
@@ -65,4 +65,6 @@ Maximum upload size:
- `10 MiB`
If a custom sound is missing or unreadable, `comfyui-ping` logs a warning and stays silent.
If a selected sound is missing or unreadable, `comfyui-ping` logs a warning and stays silent.
Uploaded sounds are stored alongside the bundled sounds in `sounds/`.
Older installs with `sounds/bundled`, `sounds/custom`, or `input/ping` are migrated into `sounds/` automatically.
+2 -2
View File
@@ -33,8 +33,8 @@ def normalize_sound_id(sound_id: str | None) -> str | None:
if sound_id is None or sound_id == "":
return None
if sound_id.startswith(SOUND_PREFIXES):
return sound_id
return f"bundled:{sound_id}"
return sound_id.split(":", 1)[1]
return sound_id
def build_notification_payload(
+38 -77
View File
@@ -3,20 +3,18 @@ from typing import Protocol
from .runtime import get_runtime_settings, update_runtime_settings
from .sounds import (
BUNDLED_SOUNDS_DIR,
CUSTOM_SOUNDS_DIR,
LEGACY_CUSTOM_SOUNDS_DIR,
LEGACY_INPUT_SOUNDS_DIR,
MAX_UPLOAD_BYTES,
ensure_custom_sound_storage,
SOUNDS_DIR,
ensure_sound_storage,
is_allowed_upload,
list_available_sounds,
list_bundled_sounds,
list_custom_sounds,
list_sound_files,
)
SOUND_CATALOG_ROUTE = "/comfyui-ping/sounds"
SOUND_UPLOAD_ROUTE = "/comfyui-ping/sounds/upload"
SOUND_FILE_ROUTE = "/comfyui-ping/sounds/{source}/{filename}"
SOUND_FILE_ROUTE = "/comfyui-ping/sounds/{filename}"
SETTINGS_ROUTE = "/comfyui-ping/settings"
_SOUND_ROUTES_REGISTERED = False
@@ -28,48 +26,32 @@ class RouteRegistrar(Protocol):
def build_sound_catalog_payload(
*,
bundled: list[str],
custom: list[str],
sounds: list[str],
) -> dict[str, list[dict[str, str]]]:
return {
"sounds": list_available_sounds(
bundled=bundled,
custom=custom,
)
}
return {"sounds": list_available_sounds(sounds)}
def resolve_sound_path(
*,
filename: str,
source: str,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> Path | None:
safe_name = Path(filename).name
if source == "bundled":
candidate = bundled_dir / safe_name
return candidate if candidate.is_file() else None
if source == "custom":
ensure_custom_sound_storage(
custom_dir=custom_dir,
legacy_custom_dir=legacy_custom_dir,
bundled_dir=bundled_dir,
)
candidate = custom_dir / safe_name
return candidate if candidate.is_file() else None
return None
ensure_sound_storage(
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
candidate = sounds_dir / safe_name
return candidate if candidate.is_file() else None
def save_uploaded_sound(
*,
filename: str,
data: bytes,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> Path:
if not is_allowed_upload(filename):
raise ValueError("Invalid audio file format")
@@ -77,35 +59,26 @@ def save_uploaded_sound(
raise ValueError("Sound file exceeds 10 MiB limit")
safe_name = Path(filename).name
if (bundled_dir / safe_name).is_file():
raise ValueError("Filename collides with bundled sound")
ensure_custom_sound_storage(
custom_dir=custom_dir,
ensure_sound_storage(
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
bundled_dir=bundled_dir,
)
target_path = custom_dir / safe_name
target_path = sounds_dir / safe_name
if target_path.is_file():
raise ValueError("Filename already exists in custom sounds")
raise ValueError("Filename already exists")
target_path.write_bytes(data)
return target_path
def list_sounds_route_payload(
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> dict[str, list[dict[str, str]]]:
ensure_custom_sound_storage(
custom_dir=custom_dir,
ensure_sound_storage(
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
bundled_dir=bundled_dir,
)
return build_sound_catalog_payload(
bundled=list_bundled_sounds(bundled_dir),
custom=list_custom_sounds(custom_dir),
)
return build_sound_catalog_payload(list_sound_files(sounds_dir))
def runtime_settings_route_payload() -> dict[str, object]:
@@ -114,16 +87,12 @@ def runtime_settings_route_payload() -> dict[str, object]:
def serve_sound_route_path(
filename: str,
source: str,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> Path | None:
return resolve_sound_path(
filename=filename,
source=source,
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
@@ -132,16 +101,14 @@ def attach_sound_routes(
*,
routes: RouteRegistrar,
web_module: object,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> None:
@routes.get(SOUND_CATALOG_ROUTE)
async def list_sounds(_request):
return web_module.json_response(
list_sounds_route_payload(
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
)
@@ -165,9 +132,7 @@ def attach_sound_routes(
async def serve_sound(request):
path = serve_sound_route_path(
filename=request.match_info.get("filename", ""),
source=request.match_info.get("source", ""),
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
if path is None:
@@ -195,8 +160,7 @@ def attach_sound_routes(
save_uploaded_sound(
filename=filename,
data=data,
custom_dir=custom_dir,
bundled_dir=bundled_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
except ValueError as exc:
@@ -207,8 +171,7 @@ def attach_sound_routes(
return web_module.json_response(
list_sounds_route_payload(
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
),
status=201,
@@ -217,9 +180,8 @@ def attach_sound_routes(
def register_sound_routes(
*,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> bool:
global _SOUND_ROUTES_REGISTERED
@@ -240,8 +202,7 @@ def register_sound_routes(
attach_sound_routes(
routes=routes,
web_module=web,
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
_SOUND_ROUTES_REGISTERED = True
+2 -2
View File
@@ -19,8 +19,8 @@ DEFAULT_RUNTIME_SETTINGS: RuntimeSettings = {
"notify_mode": "queue_drained",
"success_enabled": True,
"failure_enabled": True,
"success_sound": "bundled:ping-success.wav",
"failure_sound": "bundled:ping-failure.wav",
"success_sound": "ping-success.wav",
"failure_sound": "ping-failure.wav",
"volume": 0.8,
}
+25 -46
View File
@@ -1,41 +1,27 @@
from collections.abc import Iterable
from pathlib import Path
from typing import Literal, TypedDict
from typing import TypedDict
REPO_ROOT = Path(__file__).resolve().parents[1]
SOUNDS_DIR = REPO_ROOT / "sounds"
BUNDLED_SOUNDS_DIR = SOUNDS_DIR / "bundled"
CUSTOM_SOUNDS_DIR = SOUNDS_DIR / "custom"
LEGACY_CUSTOM_SOUNDS_DIR = REPO_ROOT / "input" / "ping"
LEGACY_BUNDLED_SOUNDS_DIR = SOUNDS_DIR / "bundled"
LEGACY_CUSTOM_SOUNDS_DIR = SOUNDS_DIR / "custom"
LEGACY_INPUT_SOUNDS_DIR = REPO_ROOT / "input" / "ping"
ALLOWED_EXTENSIONS = {".wav", ".mp3", ".ogg", ".m4a", ".flac"}
MAX_UPLOAD_BYTES = 10 * 1024 * 1024
class SoundCatalogEntry(TypedDict):
name: str
source: Literal["bundled", "custom"]
def is_allowed_upload(filename: str) -> bool:
return Path(filename).suffix.lower() in ALLOWED_EXTENSIONS
def list_available_sounds(
*,
bundled: Iterable[str],
custom: Iterable[str],
) -> list[SoundCatalogEntry]:
return [
*(
{"name": name, "source": "bundled"}
for name in bundled
),
*(
{"name": name, "source": "custom"}
for name in custom
),
]
def list_available_sounds(sounds: Iterable[str]) -> list[SoundCatalogEntry]:
return [{"name": name} for name in sounds]
def list_sound_files(directory: Path) -> list[str]:
@@ -49,36 +35,29 @@ def list_sound_files(directory: Path) -> list[str]:
)
def list_bundled_sounds(bundled_dir: Path = BUNDLED_SOUNDS_DIR) -> list[str]:
return list_sound_files(bundled_dir)
def list_custom_sounds(custom_dir: Path = CUSTOM_SOUNDS_DIR) -> list[str]:
return list_sound_files(custom_dir)
def ensure_custom_sound_storage(
custom_dir: Path = CUSTOM_SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_CUSTOM_SOUNDS_DIR,
bundled_dir: Path = BUNDLED_SOUNDS_DIR,
def ensure_sound_storage(
sounds_dir: Path = SOUNDS_DIR,
legacy_custom_dir: Path = LEGACY_INPUT_SOUNDS_DIR,
) -> Path:
custom_dir.mkdir(parents=True, exist_ok=True)
if not legacy_custom_dir.exists():
return custom_dir
sounds_dir.mkdir(parents=True, exist_ok=True)
for legacy_file in legacy_custom_dir.iterdir():
if not legacy_file.is_file() or not is_allowed_upload(legacy_file.name):
for legacy_dir in (
LEGACY_BUNDLED_SOUNDS_DIR,
LEGACY_CUSTOM_SOUNDS_DIR,
legacy_custom_dir,
):
if legacy_dir == sounds_dir or not legacy_dir.exists():
continue
if (bundled_dir / legacy_file.name).exists():
legacy_file.unlink(missing_ok=True)
continue
for legacy_file in legacy_dir.iterdir():
if not legacy_file.is_file() or not is_allowed_upload(legacy_file.name):
continue
target_path = custom_dir / legacy_file.name
if target_path.exists():
legacy_file.unlink(missing_ok=True)
continue
target_path = sounds_dir / legacy_file.name
if target_path.exists():
legacy_file.unlink(missing_ok=True)
continue
legacy_file.replace(target_path)
legacy_file.replace(target_path)
return custom_dir
return sounds_dir
+11 -31
View File
@@ -1,22 +1,16 @@
import { SOUND_SOURCES } from "./constants";
export type SoundSource =
(typeof SOUND_SOURCES)[keyof typeof SOUND_SOURCES];
export interface FetchApiClient {
fetchApi: (route: string, options?: RequestInit) => Promise<Response>;
}
export interface SoundCatalogEntry {
name: string;
source: SoundSource;
}
export interface SoundCatalogPayload {
sounds: SoundCatalogEntry[];
}
export type SoundOptionId = `${SoundSource}:${string}`;
export type SoundOptionId = string;
export interface RuntimeNotificationSettings {
enabled: boolean;
notify_mode: "every_prompt" | "queue_drained";
@@ -32,7 +26,7 @@ export const SOUND_UPLOAD_ROUTE = "/comfyui-ping/sounds/upload";
export const SETTINGS_ROUTE = "/comfyui-ping/settings";
export function toSoundOptionId(entry: SoundCatalogEntry): SoundOptionId {
return `${entry.source}:${entry.name}`;
return entry.name;
}
export function normalizeSoundOptionId(
@@ -42,28 +36,21 @@ export function normalizeSoundOptionId(
return null;
}
if (soundId.includes(":")) {
return toSoundOptionId(parseSoundOptionId(soundId));
if (soundId.startsWith("bundled:") || soundId.startsWith("custom:")) {
return soundId.split(":")[1] ?? null;
}
return `bundled:${soundId}`;
return soundId;
}
export function parseSoundOptionId(soundId: string): SoundCatalogEntry {
const separatorIndex = soundId.indexOf(":");
const source = soundId.slice(0, separatorIndex);
const name = soundId.slice(separatorIndex + 1);
if (
source !== SOUND_SOURCES.BUNDLED &&
source !== SOUND_SOURCES.CUSTOM
) {
throw new Error(`Unsupported sound source: ${source}`);
const normalizedSoundId = normalizeSoundOptionId(soundId);
if (!normalizedSoundId) {
throw new Error("Missing sound option id");
}
return {
name,
source,
name: normalizedSoundId,
};
}
@@ -80,31 +67,25 @@ export function buildSettingsRoute(): string {
}
export function buildSoundFileRoute(entry: SoundCatalogEntry): string {
return `${SOUND_CATALOG_ROUTE}/${encodeURIComponent(entry.source)}/${encodeURIComponent(entry.name)}`;
return `${SOUND_CATALOG_ROUTE}/${encodeURIComponent(entry.name)}`;
}
export function parseSoundCatalogPayload(input: {
sounds?: Array<{
name?: string;
source?: string;
}>;
}): SoundCatalogPayload {
const sounds = input.sounds ?? [];
return {
sounds: sounds.flatMap((entry) => {
if (
typeof entry.name !== "string" ||
(entry.source !== SOUND_SOURCES.BUNDLED &&
entry.source !== SOUND_SOURCES.CUSTOM)
) {
if (typeof entry.name !== "string") {
return [];
}
return [
{
name: entry.name,
source: entry.source,
},
];
}),
@@ -121,7 +102,6 @@ async function parseCatalogResponse(
const payload: {
sounds?: Array<{
name?: string;
source?: string;
}>;
} = await response.json();
+4 -12
View File
@@ -20,7 +20,6 @@ export interface NotificationLogger {
export interface ShouldPlayNotificationInput {
selectedSound: SoundOptionId | string | null;
soundExists: boolean;
isCustom: boolean;
}
export interface PlayNotificationInput extends ShouldPlayNotificationInput {
@@ -53,8 +52,7 @@ export function catalogHasSound(
}
return catalog.some(
(catalogEntry) =>
catalogEntry.name === entry.name && catalogEntry.source === entry.source
(catalogEntry) => catalogEntry.name === entry.name
);
}
@@ -76,11 +74,7 @@ export function shouldPlayNotification(
return false;
}
if (input.isCustom && !input.soundExists) {
return false;
}
return input.soundExists || !input.isCustom;
return input.soundExists;
}
export async function playNotification(
@@ -92,9 +86,9 @@ export async function playNotification(
input.currentAudio.currentTime = 0;
}
if (input.isCustom && input.selectedSound && !input.soundExists) {
if (input.selectedSound && !input.soundExists) {
input.logger?.warn(
`Selected custom sound '${input.selectedSound}' is unavailable; skipping playback.`
`Selected sound '${input.selectedSound}' is unavailable; skipping playback.`
);
}
@@ -142,7 +136,6 @@ export async function handleNotificationEvent(input: {
}): Promise<PlayNotificationResult> {
const selectedSound = normalizeSoundOptionId(input.detail.sound_id);
const parsedSound = selectedSound ? parseSoundOptionId(selectedSound) : null;
const isCustom = parsedSound?.source === "custom";
const audioUrl = resolveNotificationAudioUrl(selectedSound);
const soundExists = parsedSound
? catalogHasSound(input.catalog, parsedSound)
@@ -152,7 +145,6 @@ export async function handleNotificationEvent(input: {
audioUrl,
createAudio: input.createAudio,
currentAudio: input.currentAudio,
isCustom,
logger: input.logger,
selectedSound,
soundExists,
+35 -30
View File
@@ -6,6 +6,7 @@ import {
} from "./audio";
import {
fetchSoundCatalog,
normalizeSoundOptionId,
storeRuntimeSettings,
toSoundOptionId,
uploadSoundFile,
@@ -83,17 +84,17 @@ interface PingLogger {
warn: (message: string) => void;
}
export const DEFAULT_SUCCESS_SOUND_ID = "bundled:ping-success.wav";
export const DEFAULT_FAILURE_SOUND_ID = "bundled:ping-failure.wav";
export const DEFAULT_SUCCESS_SOUND_ID = "ping-success.wav";
export const DEFAULT_FAILURE_SOUND_ID = "ping-failure.wav";
export const PING_EVENT_NAME = "comfyui-ping.notification";
const DEFAULT_SOUND_CATALOG: SoundCatalogPayload = {
sounds: [
{ name: "beep-ping.wav", source: "bundled" },
{ name: "harmonic-beep.wav", source: "bundled" },
{ name: "notification-soft.wav", source: "bundled" },
{ name: "ping-success.wav", source: "bundled" },
{ name: "ping-failure.wav", source: "bundled" },
{ name: "ping-ringtone.wav", source: "bundled" },
{ name: "beep-ping.wav" },
{ name: "harmonic-beep.wav" },
{ name: "notification-soft.wav" },
{ name: "ping-success.wav" },
{ name: "ping-failure.wav" },
{ name: "ping-ringtone.wav" },
],
};
@@ -108,7 +109,7 @@ export const PING_SETTINGS_IDS = {
SUCCESS_SOUND: `${SETTINGS_PREFIX}.Success Sound`,
FAILURE_SOUND: `${SETTINGS_PREFIX}.Failure Sound`,
VOLUME: `${SETTINGS_PREFIX}.Notification Volume`,
UPLOAD_ACTION: `${SETTINGS_PREFIX}.Upload Custom Sound`,
UPLOAD_ACTION: `${SETTINGS_PREFIX}.Upload Sound`,
SECTION_ADVANCED: `${SETTINGS_PREFIX}.Advanced`,
DEBUG_LOGGING: SETTINGS_IDS.DEBUG_LOGGING,
} as const;
@@ -160,7 +161,11 @@ function coerceSettingString(
value: unknown,
fallbackValue: SoundOptionId,
): string {
return typeof value === "string" && value.length > 0 ? value : fallbackValue;
if (typeof value !== "string" || value.length === 0) {
return fallbackValue;
}
return normalizeSoundOptionId(value) ?? fallbackValue;
}
function coerceSettingBoolean(value: unknown, fallbackValue: boolean): boolean {
@@ -179,22 +184,17 @@ export function buildSoundSettingOptions(
catalog: SoundCatalogPayload,
): PingSettingOption[] {
return catalog.sounds.map((entry) => ({
text: `${entry.source === "bundled" ? "Bundled" : "Custom"} / ${entry.name}`,
text: entry.name,
value: toSoundOptionId(entry),
}));
}
function buildUnavailableSoundOption(soundId: string): PingSettingOption {
const separatorIndex = soundId.indexOf(":");
const source = separatorIndex === -1 ? "" : soundId.slice(0, separatorIndex);
const name =
separatorIndex === -1 ? soundId : soundId.slice(separatorIndex + 1);
const sourceLabel =
source === "custom" ? "Custom" : source === "bundled" ? "Bundled" : "Sound";
const name = normalizeSoundOptionId(soundId) ?? soundId;
return {
text: `${sourceLabel} / ${name} (unavailable)`,
value: soundId,
text: `${name} (unavailable)`,
value: name,
};
}
@@ -202,15 +202,16 @@ export function buildRenderedSoundOptions(
options: PingSettingOption[],
selectedValue: string,
): PingSettingOption[] {
if (selectedValue.length === 0) {
const normalizedSelectedValue = normalizeSoundOptionId(selectedValue) ?? "";
if (normalizedSelectedValue.length === 0) {
return options;
}
if (options.some((option) => option.value === selectedValue)) {
if (options.some((option) => option.value === normalizedSelectedValue)) {
return options;
}
return [buildUnavailableSoundOption(selectedValue), ...options];
return [buildUnavailableSoundOption(normalizedSelectedValue), ...options];
}
function findSetting(settingId: string): PingSetting | undefined {
@@ -263,19 +264,23 @@ function populateSoundSelect(
options: PingSettingOption[],
selectedValue: string,
): void {
const renderedOptions = buildRenderedSoundOptions(options, selectedValue);
const normalizedSelectedValue = normalizeSoundOptionId(selectedValue) ?? "";
const renderedOptions = buildRenderedSoundOptions(
options,
normalizedSelectedValue,
);
selectEl.replaceChildren();
for (const option of renderedOptions) {
const optionEl = document.createElement("option");
optionEl.textContent = option.text;
optionEl.value = option.value;
optionEl.selected = option.value === selectedValue;
optionEl.selected = option.value === normalizedSelectedValue;
selectEl.append(optionEl);
}
if (selectedValue.length > 0) {
selectEl.value = selectedValue;
if (normalizedSelectedValue.length > 0) {
selectEl.value = normalizedSelectedValue;
} else if (selectEl.options.length > 0 && selectEl.selectedIndex === -1) {
selectEl.value = selectEl.options[0].value;
}
@@ -484,7 +489,7 @@ export async function tryUploadCustomSound(
return await uploadCustomSound(comfyApp, file);
} catch (error) {
logger.warn(
`Unable to upload custom sound: ${
`Unable to upload sound: ${
error instanceof Error ? error.message : String(error)
}`,
);
@@ -513,7 +518,7 @@ function createSettingsSyncOnChange(): (
function createUploadActionRenderer(): HTMLButtonElement {
const button = document.createElement("button");
button.type = "button";
button.textContent = "Upload Custom Sound";
button.textContent = "Upload Sound";
button.addEventListener("click", () => {
const runtimeApp = getRuntimeApp();
if (!runtimeApp?.api) {
@@ -636,10 +641,10 @@ export const PING_SETTINGS: PingSetting[] = [
},
{
id: PING_SETTINGS_IDS.UPLOAD_ACTION,
name: "Upload Custom Sound",
name: "Upload Sound",
type: createUploadActionRenderer,
defaultValue: undefined,
tooltip: "Upload a custom browser-played notification sound.",
tooltip: "Upload a browser-played notification sound.",
sortOrder: 2,
},
{
+7 -7
View File
@@ -1,4 +1,4 @@
import type { SoundOptionId } from "./api";
import { normalizeSoundOptionId, type SoundOptionId } from "./api";
type NotificationStatus = "success" | "failure";
@@ -17,16 +17,16 @@ export function resolveSoundSelection(
input: ResolveSoundSelectionInput
): SoundOptionId | null {
if (input.status === "success") {
return (
return normalizeSoundOptionId(
input.nodeOverrides?.successSound ??
input.globalSettings.successSound ??
null
input.globalSettings.successSound ??
null
);
}
return (
return normalizeSoundOptionId(
input.nodeOverrides?.failureSound ??
input.globalSettings.failureSound ??
null
input.globalSettings.failureSound ??
null
);
}
+14 -18
View File
@@ -5,6 +5,7 @@ import {
buildSettingsRoute,
buildSoundUploadRoute,
fetchSoundCatalog,
normalizeSoundOptionId,
parseSoundCatalogPayload,
parseSoundOptionId,
storeRuntimeSettings,
@@ -13,26 +14,22 @@ import {
} from "../src/api";
describe("sound API helpers", () => {
it("creates a stable option id from a source-aware catalog entry", () => {
it("creates a stable option id from a flat catalog entry", () => {
const entry: SoundCatalogEntry = {
name: "ping-success.wav",
source: "bundled",
};
expect(toSoundOptionId(entry)).toBe("bundled:ping-success.wav");
expect(toSoundOptionId(entry)).toBe("ping-success.wav");
});
it("parses an option id back into a source-aware entry", () => {
it("normalizes a legacy prefixed option id into a flat entry", () => {
expect(parseSoundOptionId("custom:my-sound.wav")).toEqual({
name: "my-sound.wav",
source: "custom",
});
});
it("rejects unsupported sound sources", () => {
expect(() => parseSoundOptionId("unknown:my-sound.wav")).toThrow(
"Unsupported sound source: unknown"
);
it("leaves plain option ids unchanged when normalizing", () => {
expect(normalizeSoundOptionId("my-sound.wav")).toBe("my-sound.wav");
});
it("builds the real backend routes for catalog, file, and upload requests", () => {
@@ -42,18 +39,17 @@ describe("sound API helpers", () => {
expect(
buildSoundFileRoute({
name: "my sound.wav",
source: "custom",
})
).toBe("/comfyui-ping/sounds/custom/my%20sound.wav");
).toBe("/comfyui-ping/sounds/my%20sound.wav");
});
it("parses the source-aware catalog payload", () => {
it("parses the flat catalog payload", () => {
expect(
parseSoundCatalogPayload({
sounds: [{ name: "custom.wav", source: "custom" }],
sounds: [{ name: "custom.wav" }],
})
).toEqual({
sounds: [{ name: "custom.wav", source: "custom" }],
sounds: [{ name: "custom.wav" }],
});
});
@@ -61,7 +57,7 @@ describe("sound API helpers", () => {
const fetchApi = async (): Promise<Response> =>
new Response(
JSON.stringify({
sounds: [{ name: "uploaded.wav", source: "custom" }],
sounds: [{ name: "uploaded.wav" }],
}),
{
status: 200,
@@ -70,7 +66,7 @@ describe("sound API helpers", () => {
);
await expect(fetchSoundCatalog({ fetchApi })).resolves.toEqual({
sounds: [{ name: "uploaded.wav", source: "custom" }],
sounds: [{ name: "uploaded.wav" }],
});
});
@@ -78,10 +74,10 @@ describe("sound API helpers", () => {
const settings = {
enabled: true,
failure_enabled: false,
failure_sound: "bundled:ping-failure.wav",
failure_sound: "ping-failure.wav",
notify_mode: "queue_drained",
success_enabled: true,
success_sound: "custom:uploaded.wav",
success_sound: "uploaded.wav",
volume: 0.45,
} as const;
+46 -62
View File
@@ -6,50 +6,45 @@ import {
} from "../src/audio";
describe("shouldPlayNotification", () => {
it("returns false when selected custom sound is invalid", () => {
it("returns false when the selected sound is unavailable", () => {
expect(
shouldPlayNotification({
selectedSound: "custom:missing.wav",
selectedSound: "missing.wav",
soundExists: false,
isCustom: true,
})
).toBe(false);
});
it("returns true for bundled sounds when a selection exists", () => {
it("returns true when a selected sound exists", () => {
expect(
shouldPlayNotification({
selectedSound: "bundled:ping-success.wav",
selectedSound: "ping-success.wav",
soundExists: true,
isCustom: false,
})
).toBe(true);
});
});
describe("playNotification", () => {
it("warns and plays nothing for an invalid custom sound", async () => {
it("warns and plays nothing for an invalid selected sound", async () => {
const warn = vi.fn();
const createAudio = vi.fn();
const result = await playNotification({
audioUrl: "unused",
createAudio,
isCustom: true,
logger: { warn },
selectedSound: "custom:missing.wav",
selectedSound: "missing.wav",
soundExists: false,
volume: 0.7,
});
expect(result.played).toBe(false);
expect(createAudio).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("custom sound")
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Selected sound"));
});
it("clears stale current audio for an invalid custom sound", async () => {
it("clears stale current audio for an invalid selected sound", async () => {
const pause = vi.fn();
const warn = vi.fn();
const currentAudio = {
@@ -63,9 +58,8 @@ describe("playNotification", () => {
audioUrl: "unused",
createAudio: vi.fn(),
currentAudio,
isCustom: true,
logger: { warn },
selectedSound: "custom:missing.wav",
selectedSound: "missing.wav",
soundExists: false,
volume: 0.7,
});
@@ -74,9 +68,7 @@ describe("playNotification", () => {
expect(currentAudio.currentTime).toBe(0);
expect(result.played).toBe(false);
expect(result.audio).toBeNull();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("custom sound")
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Selected sound"));
});
it("stops the previous audio before playing a new sound", async () => {
@@ -100,8 +92,7 @@ describe("playNotification", () => {
audioUrl: "memory://ping-success",
createAudio,
currentAudio,
isCustom: false,
selectedSound: "bundled:ping-success.wav",
selectedSound: "ping-success.wav",
soundExists: true,
stopPrevious: true,
volume: 0.4,
@@ -136,9 +127,8 @@ describe("playNotification", () => {
audioUrl: "memory://ping-failure",
createAudio: vi.fn(() => nextAudio),
currentAudio,
isCustom: false,
logger: { warn },
selectedSound: "bundled:ping-failure.wav",
selectedSound: "ping-failure.wav",
soundExists: true,
volume: 0.5,
});
@@ -152,7 +142,7 @@ describe("playNotification", () => {
});
describe("handleNotificationEvent", () => {
it("plays custom sounds through the real backend sound route", async () => {
it("plays flat sound ids through the real backend sound route", async () => {
const play = vi.fn().mockResolvedValue(undefined);
const audio = {
currentTime: 0,
@@ -163,7 +153,34 @@ describe("handleNotificationEvent", () => {
const createAudio = vi.fn(() => audio);
const result = await handleNotificationEvent({
catalog: [{ name: "uploaded.wav", source: "custom" }],
catalog: [{ name: "uploaded.wav" }],
createAudio,
detail: {
event_kind: "global",
sound_id: "uploaded.wav",
source: "queue_drained",
status: "success",
volume: 0.8,
},
});
expect(createAudio).toHaveBeenCalledWith("/comfyui-ping/sounds/uploaded.wav");
expect(play).toHaveBeenCalledTimes(1);
expect(result.played).toBe(true);
});
it("normalizes legacy prefixed sound ids before playback", async () => {
const play = vi.fn().mockResolvedValue(undefined);
const audio = {
currentTime: 0,
pause: vi.fn(),
play,
volume: 0,
};
const createAudio = vi.fn(() => audio);
const result = await handleNotificationEvent({
catalog: [{ name: "uploaded.wav" }],
createAudio,
detail: {
event_kind: "global",
@@ -174,23 +191,21 @@ describe("handleNotificationEvent", () => {
},
});
expect(createAudio).toHaveBeenCalledWith(
"/comfyui-ping/sounds/custom/uploaded.wav"
);
expect(createAudio).toHaveBeenCalledWith("/comfyui-ping/sounds/uploaded.wav");
expect(play).toHaveBeenCalledTimes(1);
expect(result.played).toBe(true);
});
it("warns and stays silent for custom sounds when the catalog is empty", async () => {
it("warns and stays silent when the selected sound is not in the catalog", async () => {
const warn = vi.fn();
const createAudio = vi.fn();
const result = await handleNotificationEvent({
catalog: [],
catalog: [{ name: "ping-success.wav" }],
createAudio,
detail: {
event_kind: "global",
sound_id: "custom:uploaded.wav",
sound_id: "uploaded.wav",
source: "queue_drained",
status: "success",
volume: 0.8,
@@ -199,38 +214,7 @@ describe("handleNotificationEvent", () => {
});
expect(createAudio).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("custom sound")
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Selected sound"));
expect(result.played).toBe(false);
});
it("still plays bundled sounds when the catalog is empty", async () => {
const play = vi.fn().mockResolvedValue(undefined);
const audio = {
currentTime: 0,
pause: vi.fn(),
play,
volume: 0,
};
const createAudio = vi.fn(() => audio);
const result = await handleNotificationEvent({
catalog: [],
createAudio,
detail: {
event_kind: "global",
sound_id: "bundled:ping-success.wav",
source: "queue_drained",
status: "success",
volume: 0.8,
},
});
expect(createAudio).toHaveBeenCalledWith(
"/comfyui-ping/sounds/bundled/ping-success.wav"
);
expect(play).toHaveBeenCalledTimes(1);
expect(result.played).toBe(true);
});
});
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SOUNDS_DIR = REPO_ROOT / "sounds" / "bundled"
SOUNDS_DIR = REPO_ROOT / "sounds"
def wav_peak_amplitude(path: Path) -> int:
+3 -3
View File
@@ -53,7 +53,7 @@ def test_build_global_completion_payload_uses_global_event_kind():
assert payload.event_kind == "global"
assert payload.status == "failure"
assert payload.sound_id == "bundled:global-failure.wav"
assert payload.sound_id == "global-failure.wav"
def test_resolve_global_completion_payload_normalizes_error_status():
@@ -73,7 +73,7 @@ def test_resolve_global_completion_payload_normalizes_error_status():
assert payload is not None
assert payload.status == "failure"
assert payload.sound_id == "custom:uploaded.wav"
assert payload.sound_id == "uploaded.wav"
assert payload.volume == 0.45
@@ -131,7 +131,7 @@ def test_reinstall_updates_callbacks_after_noop_install():
assert result == "done"
assert emitted_payloads[0].event_kind == "global"
assert emitted_payloads[0].status == "success"
assert emitted_payloads[0].sound_id == "bundled:global-success.wav"
assert emitted_payloads[0].sound_id == "global-success.wav"
assert emitted_payloads[0].volume == 0.65
assert len(emitted_payloads) == 1
+5 -5
View File
@@ -18,7 +18,7 @@ def test_notification_payload_includes_status_and_sound_id():
)
assert payload.status == "failure"
assert payload.sound_id == "bundled:buzz.wav"
assert payload.sound_id == "buzz.wav"
assert payload.volume == 0.8
assert payload.source == "global"
@@ -31,7 +31,7 @@ def test_notification_policy_prefers_node_override():
node_overrides={"success_sound": "node-success.wav"},
)
assert payload.sound_id == "bundled:node-success.wav"
assert payload.sound_id == "node-success.wav"
def test_invalid_custom_sound_does_not_fallback():
@@ -52,11 +52,11 @@ def test_resolve_sound_id_uses_global_value_without_override():
global_settings={"failure_sound": "global-failure.wav"},
node_overrides=None,
)
== "bundled:global-failure.wav"
== "global-failure.wav"
)
def test_notification_policy_preserves_source_aware_sound_id():
def test_notification_policy_normalizes_legacy_prefixed_sound_id():
payload = build_notification_payload(
event_kind="node",
status="failure",
@@ -64,4 +64,4 @@ def test_notification_policy_preserves_source_aware_sound_id():
node_overrides={"failure_sound": "custom:alert.wav"},
)
assert payload.sound_id == "custom:alert.wav"
assert payload.sound_id == "alert.wav"
+1 -1
View File
@@ -130,5 +130,5 @@ def test_ping_node_execute_emits_runtime_notification_when_enabled():
assert output is not None
assert len(emitted_payloads) == 1
assert emitted_payloads[0].status == "success"
assert emitted_payloads[0].sound_id == "bundled:ding.wav"
assert emitted_payloads[0].sound_id == "ding.wav"
assert emitted_payloads[0].volume == 0.35
+49 -100
View File
@@ -20,124 +20,102 @@ from comfyui_ping.sounds import MAX_UPLOAD_BYTES
def test_build_sound_catalog_payload_returns_sound_list():
payload = build_sound_catalog_payload(
bundled=["ping-success.wav"],
custom=["custom.wav"],
)
payload = build_sound_catalog_payload(["ping-success.wav", "custom.wav"])
assert payload == {
"sounds": [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "custom.wav", "source": "custom"},
{"name": "ping-success.wav"},
{"name": "custom.wav"},
],
}
def test_list_sounds_route_payload_returns_structured_catalog(tmp_path: Path):
bundled_dir = tmp_path / "bundled"
custom_dir = tmp_path / "custom"
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "legacy"
bundled_dir.mkdir()
custom_dir.mkdir()
(bundled_dir / "ping-success.wav").write_bytes(b"bundled")
(custom_dir / "custom.wav").write_bytes(b"custom")
sounds_dir.mkdir()
(sounds_dir / "ping-success.wav").write_bytes(b"bundled")
(sounds_dir / "custom.wav").write_bytes(b"custom")
from comfyui_ping.routes import list_sounds_route_payload
payload = list_sounds_route_payload(
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
assert payload == {
"sounds": [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "custom.wav", "source": "custom"},
{"name": "custom.wav"},
{"name": "ping-success.wav"},
],
}
def test_resolve_sound_path_uses_requested_source():
bundled_dir = REPO_ROOT / "sounds" / "bundled"
custom_dir = REPO_ROOT / "sounds" / "custom"
def test_resolve_sound_path_uses_flat_directory():
sounds_dir = REPO_ROOT / "sounds"
legacy_custom_dir = REPO_ROOT / "input" / "ping-unused-test"
custom_dir.mkdir(parents=True, exist_ok=True)
custom_file = custom_dir / "ping-success.wav"
custom_file = sounds_dir / "temp-flat-route-test.wav"
custom_file.write_bytes(b"custom")
try:
bundled_path = resolve_sound_path(
filename="ping-success.wav",
source="bundled",
bundled_dir=bundled_dir,
custom_dir=custom_dir,
legacy_custom_dir=legacy_custom_dir,
)
custom_path = resolve_sound_path(
filename="ping-success.wav",
source="custom",
bundled_dir=bundled_dir,
custom_dir=custom_dir,
filename="temp-flat-route-test.wav",
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
finally:
custom_file.unlink(missing_ok=True)
assert bundled_path == bundled_dir / "ping-success.wav"
assert custom_path == custom_file
def test_list_sounds_route_payload_migrates_legacy_custom_uploads(tmp_path: Path):
bundled_dir = tmp_path / "sounds" / "bundled"
custom_dir = tmp_path / "sounds" / "custom"
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "input" / "ping"
bundled_dir.mkdir(parents=True)
sounds_dir.mkdir(parents=True)
legacy_custom_dir.mkdir(parents=True)
(bundled_dir / "ping-success.wav").write_bytes(b"bundled")
(sounds_dir / "ping-success.wav").write_bytes(b"bundled")
(legacy_custom_dir / "ringtone5.mp3").write_bytes(b"legacy")
from comfyui_ping.routes import list_sounds_route_payload
payload = list_sounds_route_payload(
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
assert payload == {
"sounds": [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "ringtone5.mp3", "source": "custom"},
{"name": "ping-success.wav"},
{"name": "ringtone5.mp3"},
],
}
assert (custom_dir / "ringtone5.mp3").read_bytes() == b"legacy"
assert (sounds_dir / "ringtone5.mp3").read_bytes() == b"legacy"
assert (legacy_custom_dir / "ringtone5.mp3").exists() is False
def test_list_sounds_route_payload_skips_legacy_files_promoted_to_bundled(
def test_list_sounds_route_payload_skips_duplicate_legacy_files(
tmp_path: Path,
):
bundled_dir = tmp_path / "sounds" / "bundled"
custom_dir = tmp_path / "sounds" / "custom"
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "input" / "ping"
bundled_dir.mkdir(parents=True)
sounds_dir.mkdir(parents=True)
legacy_custom_dir.mkdir(parents=True)
(bundled_dir / "ping-ringtone.wav").write_bytes(b"bundled")
(sounds_dir / "ping-ringtone.wav").write_bytes(b"bundled")
(legacy_custom_dir / "ping-ringtone.wav").write_bytes(b"legacy")
from comfyui_ping.routes import list_sounds_route_payload
payload = list_sounds_route_payload(
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
assert payload == {
"sounds": [{"name": "ping-ringtone.wav", "source": "bundled"}],
"sounds": [{"name": "ping-ringtone.wav"}],
}
assert (custom_dir / "ping-ringtone.wav").exists() is False
assert (sounds_dir / "ping-ringtone.wav").read_bytes() == b"bundled"
assert (legacy_custom_dir / "ping-ringtone.wav").exists() is False
@@ -146,7 +124,7 @@ def test_save_uploaded_sound_rejects_invalid_extension(tmp_path: Path):
save_uploaded_sound(
filename="not-a-sound.txt",
data=b"bad",
custom_dir=tmp_path,
sounds_dir=tmp_path,
legacy_custom_dir=tmp_path / "legacy",
)
except ValueError as exc:
@@ -160,7 +138,7 @@ def test_save_uploaded_sound_rejects_oversized_file(tmp_path: Path):
save_uploaded_sound(
filename="too-large.wav",
data=b"0" * (MAX_UPLOAD_BYTES + 1),
custom_dir=tmp_path / "custom",
sounds_dir=tmp_path / "sounds",
legacy_custom_dir=tmp_path / "legacy",
)
except ValueError as exc:
@@ -169,58 +147,33 @@ def test_save_uploaded_sound_rejects_oversized_file(tmp_path: Path):
raise AssertionError("save_uploaded_sound should reject oversized files")
def test_save_uploaded_sound_rejects_bundled_name_collision(tmp_path: Path):
bundled_dir = tmp_path / "bundled"
def test_save_uploaded_sound_rejects_existing_filename(tmp_path: Path):
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "legacy"
bundled_dir.mkdir()
(bundled_dir / "ping-success.wav").write_bytes(b"bundled")
sounds_dir.mkdir()
(sounds_dir / "ping-success.wav").write_bytes(b"existing")
try:
save_uploaded_sound(
filename="ping-success.wav",
data=b"custom",
custom_dir=tmp_path / "custom",
bundled_dir=bundled_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
except ValueError as exc:
assert str(exc) == "Filename collides with bundled sound"
assert str(exc) == "Filename already exists"
else:
raise AssertionError("save_uploaded_sound should reject bundled name collisions")
def test_save_uploaded_sound_rejects_existing_custom_filename(tmp_path: Path):
custom_dir = tmp_path / "custom"
legacy_custom_dir = tmp_path / "legacy"
custom_dir.mkdir()
(custom_dir / "custom.wav").write_bytes(b"old")
try:
save_uploaded_sound(
filename="custom.wav",
data=b"new",
custom_dir=custom_dir,
bundled_dir=tmp_path / "bundled",
legacy_custom_dir=legacy_custom_dir,
)
except ValueError as exc:
assert str(exc) == "Filename already exists in custom sounds"
else:
raise AssertionError("save_uploaded_sound should reject existing custom filenames")
raise AssertionError("save_uploaded_sound should reject existing filenames")
def test_resolve_sound_path_rejects_non_files(tmp_path: Path):
bundled_dir = tmp_path / "bundled"
custom_dir = tmp_path / "custom"
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "legacy"
(bundled_dir / "ping-success.wav").mkdir(parents=True)
custom_dir.mkdir()
(sounds_dir / "ping-success.wav").mkdir(parents=True)
path = resolve_sound_path(
filename="ping-success.wav",
source="bundled",
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
@@ -293,18 +246,15 @@ class FakeRequest:
def test_attach_sound_routes_registers_live_handlers(tmp_path: Path):
routes = FakeRoutes()
bundled_dir = tmp_path / "bundled"
custom_dir = tmp_path / "custom"
sounds_dir = tmp_path / "sounds"
legacy_custom_dir = tmp_path / "legacy"
bundled_dir.mkdir()
custom_dir.mkdir()
(bundled_dir / "ping-success.wav").write_bytes(b"bundled")
sounds_dir.mkdir()
(sounds_dir / "ping-success.wav").write_bytes(b"bundled")
attach_sound_routes(
routes=routes,
web_module=FakeWeb,
bundled_dir=bundled_dir,
custom_dir=custom_dir,
sounds_dir=sounds_dir,
legacy_custom_dir=legacy_custom_dir,
)
@@ -315,7 +265,7 @@ def test_attach_sound_routes_registers_live_handlers(tmp_path: Path):
list_handler = routes.handlers[("GET", SOUND_CATALOG_ROUTE)]
list_response = run(list_handler(FakeRequest()))
assert list_response.payload == {
"sounds": [{"name": "ping-success.wav", "source": "bundled"}]
"sounds": [{"name": "ping-success.wav"}]
}
upload_handler = routes.handlers[("POST", SOUND_UPLOAD_ROUTE)]
@@ -334,8 +284,8 @@ def test_attach_sound_routes_registers_live_handlers(tmp_path: Path):
assert upload_response.status == 201
assert upload_response.payload == {
"sounds": [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "custom.wav", "source": "custom"},
{"name": "custom.wav"},
{"name": "ping-success.wav"},
]
}
@@ -344,10 +294,9 @@ def test_attach_sound_routes_registers_live_handlers(tmp_path: Path):
serve_handler(
FakeRequest(
match_info={
"source": "custom",
"filename": "custom.wav",
}
)
)
)
assert serve_response.path == custom_dir / "custom.wav"
assert serve_response.path == sounds_dir / "custom.wav"
+14 -20
View File
@@ -12,15 +12,12 @@ from comfyui_ping.sounds import (
)
def test_list_sounds_returns_bundled_and_custom_entries():
sounds = list_available_sounds(
bundled=["ping-success.wav"],
custom=["my.wav"],
)
def test_list_sounds_returns_flat_entries():
sounds = list_available_sounds(["ping-success.wav", "my.wav"])
assert sounds == [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "my.wav", "source": "custom"},
{"name": "ping-success.wav"},
{"name": "my.wav"},
]
@@ -35,21 +32,18 @@ def test_accepts_expected_audio_extensions():
def test_bundled_sound_files_exist():
assert (REPO_ROOT / "sounds" / "bundled" / "ping-success.wav").exists()
assert (REPO_ROOT / "sounds" / "bundled" / "ping-failure.wav").exists()
assert (REPO_ROOT / "sounds" / "bundled" / "ping-ringtone.wav").exists()
assert (REPO_ROOT / "sounds" / "bundled" / "notification-soft.wav").exists()
assert (REPO_ROOT / "sounds" / "bundled" / "beep-ping.wav").exists()
assert (REPO_ROOT / "sounds" / "bundled" / "harmonic-beep.wav").exists()
assert (REPO_ROOT / "sounds" / "ping-success.wav").exists()
assert (REPO_ROOT / "sounds" / "ping-failure.wav").exists()
assert (REPO_ROOT / "sounds" / "ping-ringtone.wav").exists()
assert (REPO_ROOT / "sounds" / "notification-soft.wav").exists()
assert (REPO_ROOT / "sounds" / "beep-ping.wav").exists()
assert (REPO_ROOT / "sounds" / "harmonic-beep.wav").exists()
def test_catalog_preserves_duplicate_names_by_source():
sounds = list_available_sounds(
bundled=["ping-success.wav"],
custom=["ping-success.wav"],
)
def test_catalog_preserves_duplicate_names():
sounds = list_available_sounds(["ping-success.wav", "ping-success.wav"])
assert sounds == [
{"name": "ping-success.wav", "source": "bundled"},
{"name": "ping-success.wav", "source": "custom"},
{"name": "ping-success.wav"},
{"name": "ping-success.wav"},
]
+29 -30
View File
@@ -116,7 +116,7 @@ describe("PING_SETTINGS", () => {
expect(
PING_SETTINGS.map((setting) => setting.name)
).toEqual([
"Version 1.0.1",
"Version 1.0.2",
"Notifications",
"Enable Workflow Notifications",
"Global Notify Mode",
@@ -126,7 +126,7 @@ describe("PING_SETTINGS", () => {
"Success Sound",
"Failure Sound",
"Notification Volume",
"Upload Custom Sound",
"Upload Sound",
"Advanced",
"Enable Debug Logging",
]);
@@ -167,7 +167,7 @@ describe("PING_SETTINGS", () => {
);
});
it("uses bundled defaults for success and failure sounds", () => {
it("uses flat filename defaults for success and failure sounds", () => {
const successSetting = PING_SETTINGS.find(
(setting) => setting.id === PING_SETTINGS_IDS.SUCCESS_SOUND
);
@@ -179,11 +179,11 @@ describe("PING_SETTINGS", () => {
expect(failureSetting?.defaultValue).toBe(DEFAULT_FAILURE_SOUND_ID);
});
it("applies source-aware catalog options to both sound settings", () => {
it("applies flat catalog options to both sound settings", () => {
applySoundCatalogToSettings({
sounds: [
{ name: "ping-success.wav", source: "bundled" },
{ name: "uploaded.wav", source: "custom" },
{ name: "ping-success.wav" },
{ name: "uploaded.wav" },
],
});
@@ -195,27 +195,27 @@ describe("PING_SETTINGS", () => {
);
expect(successSetting?.options).toEqual([
{ text: "Bundled / ping-success.wav", value: "bundled:ping-success.wav" },
{ text: "Custom / uploaded.wav", value: "custom:uploaded.wav" },
{ text: "ping-success.wav", value: "ping-success.wav" },
{ text: "uploaded.wav", value: "uploaded.wav" },
]);
expect(failureSetting?.options).toEqual([
{ text: "Bundled / ping-success.wav", value: "bundled:ping-success.wav" },
{ text: "Custom / uploaded.wav", value: "custom:uploaded.wav" },
{ text: "ping-success.wav", value: "ping-success.wav" },
{ text: "uploaded.wav", value: "uploaded.wav" },
]);
});
it("preserves a persisted custom selection while the live catalog is still loading", () => {
it("preserves a persisted legacy selection while the live catalog is still loading", () => {
expect(
buildRenderedSoundOptions(
[{ text: "Bundled / ping-success.wav", value: "bundled:ping-success.wav" }],
[{ text: "ping-success.wav", value: "ping-success.wav" }],
"custom:uploaded.wav"
)
).toEqual([
{
text: "Custom / uploaded.wav (unavailable)",
value: "custom:uploaded.wav",
text: "uploaded.wav (unavailable)",
value: "uploaded.wav",
},
{ text: "Bundled / ping-success.wav", value: "bundled:ping-success.wav" },
{ text: "ping-success.wav", value: "ping-success.wav" },
]);
});
@@ -223,7 +223,7 @@ describe("PING_SETTINGS", () => {
const fetchApi = vi.fn(async () =>
new Response(
JSON.stringify({
sounds: [{ name: "uploaded.wav", source: "custom" }],
sounds: [{ name: "uploaded.wav" }],
}),
{
status: 200,
@@ -240,7 +240,7 @@ describe("PING_SETTINGS", () => {
},
})
).resolves.toEqual({
sounds: [{ name: "uploaded.wav", source: "custom" }],
sounds: [{ name: "uploaded.wav" }],
});
expect(fetchApi).toHaveBeenCalledWith("/comfyui-ping/sounds", undefined);
});
@@ -292,10 +292,10 @@ describe("PING_SETTINGS", () => {
).resolves.toEqual({
enabled: false,
failure_enabled: false,
failure_sound: "bundled:ping-failure.wav",
failure_sound: "ping-failure.wav",
notify_mode: "every_prompt",
success_enabled: true,
success_sound: "custom:uploaded.wav",
success_sound: "uploaded.wav",
volume: 0.35,
});
@@ -312,7 +312,7 @@ describe("PING_SETTINGS", () => {
const fetchApi = vi.fn(async () =>
new Response(
JSON.stringify({
error: "Filename already exists in custom sounds",
error: "Filename already exists",
}),
{
status: 400,
@@ -334,9 +334,7 @@ describe("PING_SETTINGS", () => {
)
).resolves.toBeNull();
expect(warn).toHaveBeenCalledWith(
expect.stringContaining("Unable to upload custom sound")
);
expect(warn).toHaveBeenCalledWith(expect.stringContaining("Unable to upload sound"));
expect(fetchApi).toHaveBeenCalledWith(
"/comfyui-ping/sounds/upload",
expect.objectContaining({
@@ -345,7 +343,7 @@ describe("PING_SETTINGS", () => {
);
});
it("syncs runtime settings when the custom success sound renderer changes", async () => {
it("syncs runtime settings when the success sound renderer changes", async () => {
let successSoundValue = DEFAULT_SUCCESS_SOUND_ID;
const fetchApi = vi.fn(
async (_route: string, options?: RequestInit): Promise<Response> =>
@@ -401,7 +399,7 @@ describe("PING_SETTINGS", () => {
throw new Error("Expected the renderer to return a select element");
}
selectEl.value = "bundled:ping-failure.wav";
selectEl.value = "ping-failure.wav";
selectEl.dispatchEvent(new Event("change"));
await Promise.resolve();
@@ -450,8 +448,8 @@ describe("PING_SETTINGS", () => {
applySoundCatalogToSettings({
sounds: [
{ name: "ping-success.wav", source: "bundled" },
{ name: "uploaded.wav", source: "custom" },
{ name: "ping-success.wav" },
{ name: "uploaded.wav" },
],
});
@@ -480,7 +478,7 @@ describe("PING_SETTINGS", () => {
await Promise.resolve();
expect(FakeAudio.created).toHaveLength(1);
expect(FakeAudio.created[0]?.src).toBe("/comfyui-ping/sounds/custom/uploaded.wav");
expect(FakeAudio.created[0]?.src).toBe("/comfyui-ping/sounds/uploaded.wav");
expect(FakeAudio.created[0]?.volume).toBe(0.35);
expect(FakeAudio.created[0]?.play).toHaveBeenCalledTimes(1);
@@ -490,13 +488,14 @@ describe("PING_SETTINGS", () => {
it("subscribes to ping notification events", async () => {
const addEventListener = vi.fn();
vi.stubGlobal("document", new FakeDocument());
vi.stubGlobal("app", {
api: {
addEventListener,
fetchApi: vi.fn(async () =>
new Response(
JSON.stringify({
sounds: [{ name: "ping-success.wav", source: "bundled" }],
sounds: [{ name: "ping-success.wav" }],
}),
{
status: 200,
@@ -578,7 +577,7 @@ describe("PING_SETTINGS", () => {
expect(event?.type).toBe(PING_EVENT_NAME);
expect(event?.detail).toEqual({
event_kind: "queue_error",
sound_id: "bundled:ping-failure.wav",
sound_id: "ping-failure.wav",
source: "prompt_no_outputs",
status: "failure",
volume: 0.45,
+2 -2
View File
@@ -9,7 +9,7 @@ describe("resolveSoundSelection", () => {
nodeOverrides: { successSound: "bundled:node.wav" },
globalSettings: { successSound: "bundled:global.wav" },
})
).toBe("bundled:node.wav");
).toBe("node.wav");
});
it("falls back to the matching global sound", () => {
@@ -19,7 +19,7 @@ describe("resolveSoundSelection", () => {
nodeOverrides: {},
globalSettings: { failureSound: "custom:failure.wav" },
})
).toBe("custom:failure.wav");
).toBe("failure.wav");
});
it("returns null when no matching sound is configured", () => {