|
|
|
@@ -4,10 +4,13 @@ Plays a ding/timer sound at regular intervals aligned to round period marks
|
|
|
|
(e.g. every 15 min), with an optional offset so the sound plays a few seconds
|
|
|
|
(e.g. every 15 min), with an optional offset so the sound plays a few seconds
|
|
|
|
before each mark.
|
|
|
|
before each mark.
|
|
|
|
|
|
|
|
|
|
|
|
Default: period 15 minutes, offset 15 seconds.
|
|
|
|
Default: period 15 min, offset 15 s, 1 ding, sound asterisk.
|
|
|
|
Notification plays at :14:45, :29:45, :44:45, :59:45.
|
|
|
|
Notification at :14:45, :29:45, :44:45, :59:45. Windows + Linux (stdlib).
|
|
|
|
|
|
|
|
|
|
|
|
Works on Windows and Linux (stdlib + optional system players on Linux).
|
|
|
|
python python/utils/interval_timer_ding.py
|
|
|
|
|
|
|
|
python python/utils/interval_timer_ding.py -p 30 -o 10
|
|
|
|
|
|
|
|
python python/utils/interval_timer_ding.py -s exclamation -n 2
|
|
|
|
|
|
|
|
python python/utils/interval_timer_ding.py -s /path/to/notify.wav
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import argparse
|
|
|
|
@@ -38,20 +41,54 @@ def _wait_seconds(period_sec: int, offset_sec: int) -> int:
|
|
|
|
return max(1, wait) if wait > 0 else 1
|
|
|
|
return max(1, wait) if wait > 0 else 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Gap between repeated dings (Windows MessageBeep is async so need ~0.5s+)
|
|
|
|
|
|
|
|
_DING_GAP_SEC = 1.7
|
|
|
|
|
|
|
|
_WIN_BEEP_MS = 350
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Sound choice: None = use default, "asterisk"|"exclamation"|... = Windows preset, or file path
|
|
|
|
|
|
|
|
_sound: str | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _play_ding_windows() -> None:
|
|
|
|
def _play_ding_windows() -> None:
|
|
|
|
try:
|
|
|
|
import winsound
|
|
|
|
import winsound
|
|
|
|
sound = _sound or "asterisk"
|
|
|
|
winsound.MessageBeep(winsound.MB_ICONASTERISK)
|
|
|
|
if sound in ("asterisk", "exclamation", "hand", "question", "beep"):
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
import winsound
|
|
|
|
if sound == "asterisk":
|
|
|
|
winsound.Beep(800, 200)
|
|
|
|
winsound.MessageBeep(winsound.MB_ICONASTERISK)
|
|
|
|
|
|
|
|
elif sound == "exclamation":
|
|
|
|
|
|
|
|
winsound.MessageBeep(winsound.MB_ICONEXCLAMATION)
|
|
|
|
|
|
|
|
elif sound == "hand":
|
|
|
|
|
|
|
|
winsound.MessageBeep(winsound.MB_ICONHAND)
|
|
|
|
|
|
|
|
elif sound == "question":
|
|
|
|
|
|
|
|
winsound.MessageBeep(winsound.MB_ICONQUESTION)
|
|
|
|
|
|
|
|
else: # beep
|
|
|
|
|
|
|
|
winsound.Beep(800, _WIN_BEEP_MS)
|
|
|
|
|
|
|
|
return
|
|
|
|
except Exception:
|
|
|
|
except Exception:
|
|
|
|
sys.stdout.write("\a")
|
|
|
|
pass
|
|
|
|
sys.stdout.flush()
|
|
|
|
# Custom file path (SND_FILENAME only = blocking until sound finishes)
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
winsound.PlaySound(sound, winsound.SND_FILENAME)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
winsound.Beep(800, _WIN_BEEP_MS)
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
|
|
sys.stdout.write("\a")
|
|
|
|
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _play_ding_linux() -> None:
|
|
|
|
def _play_ding_linux() -> None:
|
|
|
|
|
|
|
|
sound = _sound
|
|
|
|
|
|
|
|
if sound:
|
|
|
|
|
|
|
|
for cmd in (["paplay", sound], ["aplay", sound]):
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
subprocess.run(cmd, capture_output=True, timeout=3)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
|
|
|
|
|
|
continue
|
|
|
|
candidates = [
|
|
|
|
candidates = [
|
|
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/bell.oga"],
|
|
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/bell.oga"],
|
|
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/message.oga"],
|
|
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/message.oga"],
|
|
|
|
@@ -68,13 +105,6 @@ def _play_ding_linux() -> None:
|
|
|
|
sys.stdout.flush()
|
|
|
|
sys.stdout.flush()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def play_ding() -> None:
|
|
|
|
|
|
|
|
if sys.platform == "win32":
|
|
|
|
|
|
|
|
_play_ding_windows()
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
_play_ding_linux()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
def main() -> None:
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description="Play a ding every N minutes, offset seconds before each round mark."
|
|
|
|
description="Play a ding every N minutes, offset seconds before each round mark."
|
|
|
|
@@ -93,12 +123,31 @@ def main() -> None:
|
|
|
|
metavar="SEC",
|
|
|
|
metavar="SEC",
|
|
|
|
help="Seconds before each round mark to play (default: 15)",
|
|
|
|
help="Seconds before each round mark to play (default: 15)",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
|
|
"-s", "--sound",
|
|
|
|
|
|
|
|
type=str,
|
|
|
|
|
|
|
|
default=None,
|
|
|
|
|
|
|
|
metavar="PRESET|PATH",
|
|
|
|
|
|
|
|
help="Notification sound: Windows presets asterisk|exclamation|hand|question|beep, or path to .wav file (default: asterisk)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
parser.add_argument(
|
|
|
|
|
|
|
|
"-n", "--dings",
|
|
|
|
|
|
|
|
type=int,
|
|
|
|
|
|
|
|
default=1,
|
|
|
|
|
|
|
|
metavar="N",
|
|
|
|
|
|
|
|
help="Number of dings per notification (default: 1, max: 10)",
|
|
|
|
|
|
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
global _sound
|
|
|
|
|
|
|
|
_sound = args.sound
|
|
|
|
|
|
|
|
|
|
|
|
if args.period < 1:
|
|
|
|
if args.period < 1:
|
|
|
|
parser.error("period must be >= 1")
|
|
|
|
parser.error("period must be >= 1")
|
|
|
|
if args.offset < 0:
|
|
|
|
if args.offset < 0:
|
|
|
|
parser.error("offset must be >= 0")
|
|
|
|
parser.error("offset must be >= 0")
|
|
|
|
|
|
|
|
if not 1 <= args.dings <= 10:
|
|
|
|
|
|
|
|
parser.error("dings must be 1..10")
|
|
|
|
|
|
|
|
|
|
|
|
period_sec = args.period * 60
|
|
|
|
period_sec = args.period * 60
|
|
|
|
now = datetime.now()
|
|
|
|
now = datetime.now()
|
|
|
|
@@ -113,10 +162,14 @@ def main() -> None:
|
|
|
|
"Press Ctrl+C to stop."
|
|
|
|
"Press Ctrl+C to stop."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
play = _play_ding_windows if sys.platform == "win32" else _play_ding_linux
|
|
|
|
while True:
|
|
|
|
while True:
|
|
|
|
wait_sec = _wait_seconds(period_sec, args.offset)
|
|
|
|
wait_sec = _wait_seconds(period_sec, args.offset)
|
|
|
|
time.sleep(wait_sec)
|
|
|
|
time.sleep(wait_sec)
|
|
|
|
play_ding()
|
|
|
|
for i in range(args.dings):
|
|
|
|
|
|
|
|
play()
|
|
|
|
|
|
|
|
if i < args.dings - 1:
|
|
|
|
|
|
|
|
time.sleep(_DING_GAP_SEC)
|
|
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] ding")
|
|
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] ding")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|