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.
This commit is contained in:
2025-07-06 02:16:39 +02:00
parent 179785ce4e
commit 68800cf823

View File

@@ -5,20 +5,43 @@ Default settings require mpg123 to be installed on the host system:
brew install mpg123
"""
import subprocess
from os.path import expanduser
from functools import lru_cache
from pathlib import Path
from typing import Tuple
import prof
import subprocess
import time
_last_played = {}
def _play_sound(soundfile):
if soundfile.startswith("~/"):
soundfilenew = soundfile.replace("~", expanduser("~"), 1)
else:
soundfilenew = soundfile
@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([e for e in audioplayer.split(" ") + [soundfilenew]])
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):
@@ -52,7 +75,7 @@ def _cmd_sounds(arg1=None, arg2=None, arg3=None):
else:
prof.cons_show("No sounds set.")
return
if arg1 == "on":
@@ -66,10 +89,14 @@ def _cmd_sounds(arg1=None, arg2=None, arg3=None):
return
if arg1 == "test":
if arg2 not in ("chat", "private", "room",):
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")
@@ -80,7 +107,7 @@ def _cmd_sounds(arg1=None, arg2=None, arg3=None):
prof.cons_show("No sound file for " + arg2)
return
_play_sound(soundfile)
_play_sound(kind=arg2)
return
if arg1 == "set":
@@ -148,11 +175,27 @@ def _cmd_sounds(arg1=None, arg2=None, arg3=None):
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 = [
synopsis = [
"/sounds",
"/sounds on|off",
"/sounds set audioplayer <cmd>",
@@ -164,23 +207,35 @@ def prof_init(version, status, account_name, fulljid):
"/sounds clear chat",
"/sounds clear room",
"/sounds clear private",
"/sounds rooms add <roomjid>"
"/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."]
["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",
@@ -190,17 +245,21 @@ def prof_init(version, status, account_name, fulljid):
"/sounds test private",
"/sounds remove private",
"/sounds rooms add myroom@conference.server.org",
"/sounds on"
"/sounds on",
]
prof.register_command("/sounds", 0, 3, synopsis, description, args, examples, _cmd_sounds)
prof.register_command(
"/sounds", 0, 3, synopsis, description, args, examples, _cmd_sounds
)
prof.completer_add("/sounds", [ "set", "clear", "on", "off", "rooms", "reset" ])
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.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")
@@ -211,11 +270,11 @@ def prof_post_chat_message_display(barejid, resource, message):
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "chat", None)
if not soundfile:
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(soundfile)
_play_sound(kind="chat")
def prof_post_room_message_display(barejid, nick, message):
@@ -224,21 +283,21 @@ def prof_post_room_message_display(barejid, nick, message):
return
if my_nick == nick:
return;
return
enabled = prof.settings_boolean_get("sounds", "enabled", False)
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "room", None)
if not soundfile:
return
roomlist = prof.settings_string_list_get("sounds", "rooms")
if roomlist and (barejid not in roomlist):
return
_play_sound(soundfile)
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):
@@ -246,7 +305,8 @@ def prof_post_priv_message_display(barejid, nick, message):
if not enabled:
return
soundfile = prof.settings_string_get("sounds", "private", None)
if soundfile:
_play_sound(soundfile)
muted = prof.settings_string_list_get("sounds", "muted")
if muted and barejid in muted:
return
_play_sound(kind="private")