#!/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()