PySide6/PJSUA2-basiertes Desktop-Softphone mit: - SIP-Telefonie (Anrufe, DTMF, Halten, Transfer, Konferenz) - CardDAV-Kontaktsync mit Write-Back (Multi-Account) - Kontaktverwaltung mit Suche und Bearbeiten-Dialog - Anrufliste mit Namensauflösung - Favoriten-Panel (manuell + häufig angerufen) - BLF-Panel (SIP Presence Monitoring) - Responsives Layout (kompakt/erweitert) - Click-to-Call (tel:/sip: URI via D-Bus) - KDE-Integration (Tray, Benachrichtigungen) - PipeWire/PulseAudio Audio-Routing - AppImage Build-Support (PyInstaller + appimagetool) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.3 KiB
Python
108 lines
3.3 KiB
Python
"""Klingelton-Player - Spielt Klingelton über separates PipeWire-Gerät ab."""
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from PySide6.QtCore import QObject, QTimer
|
|
|
|
|
|
# Standard-Klingelton-Pfade
|
|
_STANDARD_KLINGELTON = Path(__file__).parent.parent / "resources" / "sounds" / "klingelton.wav"
|
|
_FREEDESKTOP_KLINGELTON = Path("/usr/share/sounds/freedesktop/stereo/phone-incoming-call.oga")
|
|
|
|
|
|
class KlingeltonPlayer(QObject):
|
|
"""Spielt Klingelton über paplay mit optionalem PipeWire-Gerät.
|
|
|
|
Nutzt paplay (PulseAudio/PipeWire CLI) mit --device für die
|
|
Ausgabe über ein dediziertes Klingelton-Gerät (z.B. Lautsprecher
|
|
statt Headset).
|
|
"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self._prozess = None
|
|
self._loop_timer = QTimer(self)
|
|
self._loop_timer.timeout.connect(self._erneut_abspielen)
|
|
self._datei = ""
|
|
self._device = ""
|
|
self._laeuft = False
|
|
|
|
def abspielen(self, datei_pfad="", device_pw_name="", loop=True):
|
|
"""Klingelton abspielen.
|
|
|
|
Args:
|
|
datei_pfad: Pfad zur Audio-Datei (leer = Standard)
|
|
device_pw_name: PipeWire-Sink-Name (leer = System-Standard)
|
|
loop: Endlosschleife bis stoppen() aufgerufen wird
|
|
"""
|
|
self.stoppen()
|
|
|
|
# Datei bestimmen
|
|
if datei_pfad and Path(datei_pfad).exists():
|
|
self._datei = datei_pfad
|
|
elif _STANDARD_KLINGELTON.exists():
|
|
self._datei = str(_STANDARD_KLINGELTON)
|
|
elif _FREEDESKTOP_KLINGELTON.exists():
|
|
self._datei = str(_FREEDESKTOP_KLINGELTON)
|
|
else:
|
|
return # Keine Audio-Datei verfügbar
|
|
|
|
self._device = device_pw_name
|
|
self._laeuft = True
|
|
self._paplay_starten()
|
|
|
|
if loop:
|
|
# Nach 5 Sekunden erneut abspielen (Klingelton-Datei ist ~5s)
|
|
self._loop_timer.start(5500)
|
|
|
|
def stoppen(self):
|
|
"""Klingelton stoppen."""
|
|
self._laeuft = False
|
|
self._loop_timer.stop()
|
|
if self._prozess and self._prozess.poll() is None:
|
|
try:
|
|
self._prozess.terminate()
|
|
except OSError:
|
|
pass
|
|
self._prozess = None
|
|
|
|
def test_abspielen(self, datei_pfad="", device_pw_name=""):
|
|
"""Einmal abspielen zum Testen (kein Loop)."""
|
|
self.abspielen(datei_pfad, device_pw_name, loop=False)
|
|
|
|
@property
|
|
def laeuft(self):
|
|
"""True wenn Klingelton gerade abgespielt wird."""
|
|
return self._laeuft
|
|
|
|
def _paplay_starten(self):
|
|
"""paplay Subprocess starten."""
|
|
if not self._laeuft:
|
|
return
|
|
|
|
cmd = ["paplay"]
|
|
if self._device:
|
|
cmd.extend(["--device", self._device])
|
|
cmd.append(self._datei)
|
|
|
|
try:
|
|
self._prozess = subprocess.Popen(
|
|
cmd,
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except (FileNotFoundError, OSError):
|
|
self._laeuft = False
|
|
|
|
def _erneut_abspielen(self):
|
|
"""Timer-Callback: Klingelton in Schleife abspielen."""
|
|
if not self._laeuft:
|
|
self._loop_timer.stop()
|
|
return
|
|
|
|
# Vorherigen Prozess abräumen
|
|
if self._prozess and self._prozess.poll() is not None:
|
|
self._prozess = None
|
|
|
|
self._paplay_starten()
|