Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -51,7 +51,7 @@ Defaults:
- success: `ping-success.wav`
- failure: `ping-failure.wav`

## Custom sounds
## Uploads

Supported formats:

Expand All @@ -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.
4 changes: 2 additions & 2 deletions comfyui_ping/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
115 changes: 38 additions & 77 deletions comfyui_ping/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@

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
Expand All @@ -28,84 +26,59 @@ def post(self, path: str): ...


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")
if len(data) > MAX_UPLOAD_BYTES:
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]:
Expand All @@ -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,
)

Expand All @@ -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,
)
)
Expand All @@ -165,9 +132,7 @@ async def update_runtime_settings_route(request):
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:
Expand Down Expand Up @@ -195,8 +160,7 @@ async def upload_sound(request):
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:
Expand All @@ -207,8 +171,7 @@ async def upload_sound(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,
),
status=201,
Expand All @@ -217,9 +180,8 @@ async def upload_sound(request):

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

Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions comfyui_ping/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class RuntimeSettings(TypedDict):
"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,
}

Expand Down
73 changes: 26 additions & 47 deletions comfyui_ping/sounds.py
Original file line number Diff line number Diff line change
@@ -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]:
Expand All @@ -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

for legacy_file in legacy_custom_dir.iterdir():
if not legacy_file.is_file() or not is_allowed_upload(legacy_file.name):
sounds_dir.mkdir(parents=True, exist_ok=True)

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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading