Merge pull request #2 from PBandDev/flatten-sounds
refactor: flatten sound storage
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user