Files
cproof-plugins/stable/sounds.py
Jabber Developer 68800cf823 feat(sounds): add per-JID mute support and enhance playback behavior
Some users requested the ability to mute notifications from specific JIDs
without disabling all sounds. This change introduces `mute` and `unmute`
commands to allow fine-grained control over sound playback.

While implementing this, the sound path expansion logic was improved by
switching to `pathlib` with `lru_cache` for efficiency and clarity.
Playback is now also rate-limited to avoid overlapping audio triggers
from rapid message bursts.

This lays the groundwork for more user-specific customization of sound
notifications while keeping the experience smooth and reliable.

Additionally the bug with ignoring sound's `kind` was fixed: now the plugin respects specific sounds set for rooms, chats and private messages within rooms.

Finally, code was formatted via Black to avoid ambiguity in the formatting.
2025-07-06 02:16:39 +02:00

313 lines
10 KiB
Python

"""
Play sounds on various events.
Default settings require mpg123 to be installed on the host system:
sudo apt-get install mpg123
brew install mpg123
"""
from functools import lru_cache
from pathlib import Path
from typing import Tuple
import prof
import subprocess
import time
_last_played = {}
@lru_cache(maxsize=16)
def get_expanded_path(path_str: str) -> Tuple[Path, str]:
path = Path(path_str).expanduser()
return path, str(path)
def _play_sound_file(soundfile: str):
soundfile_path, soundfile_str = get_expanded_path(soundfile)
if not soundfile_path.exists():
prof.cons_show(f"Sound file not found: {soundfile_str}")
return
now = time.monotonic()
if _last_played.get(soundfile_str, 0) + 0.5 > now:
return # skip if played too recently
_last_played[soundfile_str] = now
audioplayer = prof.settings_string_get("sounds", "audioplayer", "mpg123 -q")
subprocess.Popen([*audioplayer.split(), soundfile_str], start_new_session=True)
def _play_sound(kind: str = "chat"):
soundfile = prof.settings_string_get("sounds", kind, None)
if soundfile:
_play_sound_file(soundfile)
def _cmd_sounds(arg1=None, arg2=None, arg3=None):
if not arg1:
prof.cons_show("")
enabled = prof.settings_boolean_get("sounds", "enabled", False)
chatsound = prof.settings_string_get("sounds", "chat", None)
roomsound = prof.settings_string_get("sounds", "room", None)
privatesound = prof.settings_string_get("sounds", "private", None)
roomlist = prof.settings_string_list_get("sounds", "rooms")
audioplayer = prof.settings_string_get("sounds", "audioplayer", "mpg123 -q")
if chatsound or roomsound or privatesound:
if enabled:
prof.cons_show("Sounds: ON")
else:
prof.cons_show("Sounds: OFF")
prof.cons_show("Audio Player: " + audioplayer)
if chatsound:
prof.cons_show(" Chat : " + chatsound)
if roomsound:
prof.cons_show(" Room : " + roomsound)
if roomlist and len(roomlist) > 0:
for room in roomlist:
prof.cons_show(" " + room)
else:
prof.cons_show(" All rooms")
if privatesound:
prof.cons_show(" Private : " + privatesound)
else:
prof.cons_show("No sounds set.")
return
if arg1 == "on":
prof.settings_boolean_set("sounds", "enabled", True)
prof.cons_show("Sounds enabled")
return
if arg1 == "off":
prof.settings_boolean_set("sounds", "enabled", False)
prof.cons_show("Sounds disabled")
return
if arg1 == "test":
if arg2 not in (
"chat",
"private",
"room",
):
prof.cons_bad_cmd_usage("/sounds")
return
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
prof.cons_show("Sounds disabled")
return
soundfile = prof.settings_string_get("sounds", arg2, None)
if not soundfile:
prof.cons_show("No sound file for " + arg2)
return
_play_sound(kind=arg2)
return
if arg1 == "set":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", arg3)
prof.cons_show("Set chat sound: " + arg3)
elif arg2 == "room":
prof.settings_string_set("sounds", "room", arg3)
prof.cons_show("Set room sound: " + arg3)
elif arg2 == "private":
prof.settings_string_set("sounds", "private", arg3)
prof.cons_show("Set private sound: " + arg3)
elif arg2 == "audioplayer":
prof.settings_string_set("sounds", "audioplayer", arg3)
prof.cons_show("Set audio player: " + arg3)
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "reset":
if arg2 == "audioplayer":
prof.settings_string_set("sounds", "audioplayer", "mpg123 -q")
prof.cons_show("Reset audio player to default.")
else:
prof.cons_bad_cmd_usage("/sounds")
if arg1 == "clear":
if arg2 == "chat":
prof.settings_string_set("sounds", "chat", "")
prof.cons_show("Removed chat sound.")
elif arg2 == "room":
prof.settings_string_set("sounds", "room", "")
prof.cons_show("Removed room sound.")
elif arg2 == "private":
prof.settings_string_set("sounds", "private", "")
prof.cons_show("Removed private sound.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "rooms":
if arg2 == "add":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_add("sounds", "rooms", arg3)
prof.cons_show("Sounds enabled for room: " + arg3)
elif arg2 == "remove":
if not arg3:
prof.cons_bad_cmd_usage("/sounds")
else:
prof.settings_string_list_remove("sounds", "rooms", arg3)
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and len(roomlist) > 0:
prof.cons_show("Sounds disabled for room: " + arg3)
else:
prof.cons_show("Empty room list for sounds, playing in all rooms.")
elif arg2 == "clear":
prof.settings_string_list_clear("sounds", "rooms")
prof.cons_show("Cleared sounds room list, playing in all rooms.")
else:
prof.cons_bad_cmd_usage("/sounds")
return
if arg1 == "mute":
if not arg2:
prof.cons_bad_cmd_usage("/sounds mute <jid>")
else:
prof.settings_string_list_add("sounds", "muted", arg2)
prof.cons_show("Muted JID: " + arg2)
return
if arg1 == "unmute":
if not arg2:
prof.cons_bad_cmd_usage("/sounds unmute <jid>")
else:
prof.settings_string_list_remove("sounds", "muted", arg2)
prof.cons_show("Unmuted JID: " + arg2)
return
prof.cons_bad_cmd_usage("/sounds")
def prof_init(version, status, account_name, fulljid):
synopsis = [
"/sounds",
"/sounds on|off",
"/sounds set audioplayer <cmd>",
"/sounds reset audioplayer",
"/sounds set chat <file>",
"/sounds set room <file>",
"/sounds set private <file>",
"/sounds test private|room|chat",
"/sounds clear chat",
"/sounds clear room",
"/sounds clear private",
"/sounds rooms add <roomjid>",
]
description = "Play sounds on various Profanity events. Calling with no args shows current sound files."
args = [
["on|off", "Enable or disable playing sounds."],
[
"set audioplayer <cmd>",
"Commmand line of audio player to use for playing sound files. Defaults to mpg123.",
],
["reset audioplayer", "Resets the audio player to the default mpg123."],
["set chat <file>", "Path to sound file to play on chat messages."],
["set room <file>", "Path to sound file to play on room messages."],
["set private <file>", "Path to sound file to play on private room messages."],
["test private|room|chat", "Test the configured audio player and sound file."],
["clear chat", "Remove the sound for chat messages."],
["clear room", "Remove the sound for room messages."],
["clear private", "Remove the sound for private messages."],
[
"rooms add <roomjid>",
"Add the room to the list that will play the room sound.",
],
[
"rooms remove <roomjid>",
"Remove the room from the list that will play the room sound.",
],
[
"rooms clear",
"Clear the room list, all rooms will play sounds on new messages.",
],
]
examples = [
"/sounds set audioplayer paplay",
"/sounds set chat ~/sounds/woof.mp3",
"/sounds set room ~/sounds/meow.mp3",
"/sounds set private ~/sounds/shhh.mp3",
"/sounds test private",
"/sounds remove private",
"/sounds rooms add myroom@conference.server.org",
"/sounds on",
]
prof.register_command(
"/sounds", 0, 3, synopsis, description, args, examples, _cmd_sounds
)
prof.completer_add(
"/sounds", ["set", "clear", "on", "off", "rooms", "reset", "mute", "unmute"]
)
prof.completer_add("/sounds set", ["chat", "room", "private", "audioplayer"])
prof.completer_add("/sounds reset", ["audioplayer"])
prof.completer_add("/sounds test", ["chat", "room", "private"])
prof.completer_add("/sounds clear", ["chat", "room", "private"])
prof.completer_add("/sounds rooms", ["add", "remove", "clear"])
prof.filepath_completer_add("/sounds set chat")
prof.filepath_completer_add("/sounds set room")
prof.filepath_completer_add("/sounds set private")
def prof_post_chat_message_display(barejid, resource, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(kind="chat")
def prof_post_room_message_display(barejid, nick, message):
my_nick = prof.get_room_nick(barejid)
if not my_nick:
return
if my_nick == nick:
return
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and (barejid not in roomlist):
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(soundfile, kind="room")
def prof_post_priv_message_display(barejid, nick, message):
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(kind="private")