125 lines
3.5 KiB
Python
125 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
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
|
|
before each mark.
|
|
|
|
Default: period 15 minutes, offset 15 seconds.
|
|
Notification plays at :14:45, :29:45, :44:45, :59:45.
|
|
|
|
Works on Windows and Linux (stdlib + optional system players on Linux).
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
|
|
|
|
def _seconds_since_midnight(dt: datetime) -> int:
|
|
return dt.hour * 3600 + dt.minute * 60 + dt.second
|
|
|
|
|
|
def _next_ring_seconds(period_sec: int, offset_sec: int, now_sec: int) -> int:
|
|
next_boundary = math.ceil(now_sec / period_sec) * period_sec
|
|
ring_sec = next_boundary - offset_sec
|
|
if ring_sec <= now_sec:
|
|
ring_sec += period_sec
|
|
return ring_sec
|
|
|
|
|
|
def _wait_seconds(period_sec: int, offset_sec: int) -> int:
|
|
now = datetime.now()
|
|
now_sec = _seconds_since_midnight(now)
|
|
ring_sec = _next_ring_seconds(period_sec, offset_sec, now_sec)
|
|
wait = ring_sec - now_sec
|
|
return max(1, wait) if wait > 0 else 1
|
|
|
|
|
|
def _play_ding_windows() -> None:
|
|
try:
|
|
import winsound
|
|
winsound.MessageBeep(winsound.MB_ICONASTERISK)
|
|
except Exception:
|
|
try:
|
|
import winsound
|
|
winsound.Beep(800, 200)
|
|
except Exception:
|
|
sys.stdout.write("\a")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def _play_ding_linux() -> None:
|
|
candidates = [
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/bell.oga"],
|
|
["paplay", "/usr/share/sounds/freedesktop/stereo/message.oga"],
|
|
["aplay", "/usr/share/sounds/alsa/Front_Center.wav"],
|
|
["aplay", "/usr/share/sounds/speech-dispatcher/test.wav"],
|
|
]
|
|
for cmd in candidates:
|
|
try:
|
|
subprocess.run(cmd, capture_output=True, timeout=2)
|
|
return
|
|
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
continue
|
|
sys.stdout.write("\a")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def play_ding() -> None:
|
|
if sys.platform == "win32":
|
|
_play_ding_windows()
|
|
else:
|
|
_play_ding_linux()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Play a ding every N minutes, offset seconds before each round mark."
|
|
)
|
|
parser.add_argument(
|
|
"-p", "--period",
|
|
type=int,
|
|
default=15,
|
|
metavar="MIN",
|
|
help="Interval in minutes between notifications (default: 15)",
|
|
)
|
|
parser.add_argument(
|
|
"-o", "--offset",
|
|
type=int,
|
|
default=15,
|
|
metavar="SEC",
|
|
help="Seconds before each round mark to play (default: 15)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
if args.period < 1:
|
|
parser.error("period must be >= 1")
|
|
if args.offset < 0:
|
|
parser.error("offset must be >= 0")
|
|
|
|
period_sec = args.period * 60
|
|
now = datetime.now()
|
|
now_sec = _seconds_since_midnight(now)
|
|
next_ring = _next_ring_seconds(period_sec, args.offset, now_sec)
|
|
h = next_ring // 3600
|
|
m = (next_ring % 3600) // 60
|
|
s = next_ring % 60
|
|
print(
|
|
f"Period: {args.period} min, offset: {args.offset} s. "
|
|
f"Next ding at {h:02d}:{m:02d}:{s:02d} (then every {args.period} min). "
|
|
"Press Ctrl+C to stop."
|
|
)
|
|
|
|
while True:
|
|
wait_sec = _wait_seconds(period_sec, args.offset)
|
|
time.sleep(wait_sec)
|
|
play_ding()
|
|
print(f"[{datetime.now().strftime('%H:%M:%S')}] ding")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|