netaudio-lib 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. netaudio_lib-0.0.1/.gitignore +7 -0
  2. netaudio_lib-0.0.1/PKG-INFO +31 -0
  3. netaudio_lib-0.0.1/README.md +17 -0
  4. netaudio_lib-0.0.1/pyproject.toml +24 -0
  5. netaudio_lib-0.0.1/src/netaudio_lib/__init__.py +21 -0
  6. netaudio_lib-0.0.1/src/netaudio_lib/common/app_config.py +96 -0
  7. netaudio_lib-0.0.1/src/netaudio_lib/common/mdns_cache.py +73 -0
  8. netaudio_lib-0.0.1/src/netaudio_lib/common/socket_path.py +44 -0
  9. netaudio_lib-0.0.1/src/netaudio_lib/daemon/__init__.py +2 -0
  10. netaudio_lib-0.0.1/src/netaudio_lib/daemon/client.py +73 -0
  11. netaudio_lib-0.0.1/src/netaudio_lib/daemon/server.py +574 -0
  12. netaudio_lib-0.0.1/src/netaudio_lib/dante/__init__.py +0 -0
  13. netaudio_lib-0.0.1/src/netaudio_lib/dante/application.py +238 -0
  14. netaudio_lib-0.0.1/src/netaudio_lib/dante/browser.py +381 -0
  15. netaudio_lib-0.0.1/src/netaudio_lib/dante/channel.py +97 -0
  16. netaudio_lib-0.0.1/src/netaudio_lib/dante/const.py +627 -0
  17. netaudio_lib-0.0.1/src/netaudio_lib/dante/debug_formatter.py +488 -0
  18. netaudio_lib-0.0.1/src/netaudio_lib/dante/device.py +233 -0
  19. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_commands.py +351 -0
  20. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_network.py +212 -0
  21. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_operations.py +127 -0
  22. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_parser.py +294 -0
  23. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_protocol.py +134 -0
  24. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_serializer.py +89 -0
  25. netaudio_lib-0.0.1/src/netaudio_lib/dante/device_xml_serializer.py +163 -0
  26. netaudio_lib-0.0.1/src/netaudio_lib/dante/events.py +91 -0
  27. netaudio_lib-0.0.1/src/netaudio_lib/dante/packet_store.py +381 -0
  28. netaudio_lib-0.0.1/src/netaudio_lib/dante/protocol.py +510 -0
  29. netaudio_lib-0.0.1/src/netaudio_lib/dante/service.py +131 -0
  30. netaudio_lib-0.0.1/src/netaudio_lib/dante/services/__init__.py +11 -0
  31. netaudio_lib-0.0.1/src/netaudio_lib/dante/services/arc.py +183 -0
  32. netaudio_lib-0.0.1/src/netaudio_lib/dante/services/cmc.py +130 -0
  33. netaudio_lib-0.0.1/src/netaudio_lib/dante/services/notification.py +164 -0
  34. netaudio_lib-0.0.1/src/netaudio_lib/dante/services/settings.py +68 -0
  35. netaudio_lib-0.0.1/src/netaudio_lib/dante/subscription.py +146 -0
  36. netaudio_lib-0.0.1/src/netaudio_lib/dante/transport.py +111 -0
  37. netaudio_lib-0.0.1/src/netaudio_lib/dante/tshark_capture.py +216 -0
  38. netaudio_lib-0.0.1/src/netaudio_lib/utils/__init__.py +17 -0
  39. netaudio_lib-0.0.1/src/netaudio_lib/utils/timeout.py +35 -0
@@ -0,0 +1,7 @@
1
+ *.pyc
2
+ .eggs
3
+ .sw*
4
+ dist/
5
+ __pycache__
6
+ debug.log
7
+ .hypothesis/
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.4
2
+ Name: netaudio-lib
3
+ Version: 0.0.1
4
+ Summary: Python library for controlling Audinate Dante network audio devices
5
+ Project-URL: Repository, https://github.com/chris-ritsen/network-audio-controller
6
+ Author-email: Christopher Ritsen <chris.ritsen@gmail.com>
7
+ License-Expression: Unlicense
8
+ Keywords: audinate,audio,dante,network
9
+ Requires-Python: >=3.9
10
+ Requires-Dist: ifaddr>=0.2.0
11
+ Requires-Dist: sqlitedict>=1.7.0
12
+ Requires-Dist: zeroconf>=0.38.3
13
+ Description-Content-Type: text/markdown
14
+
15
+ # netaudio-lib
16
+
17
+ Python library for controlling Audinate Dante network audio devices.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install netaudio-lib
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```python
28
+ from netaudio_lib import DanteBrowser
29
+
30
+ browser = DanteBrowser(mdns_timeout=5)
31
+ ```
@@ -0,0 +1,17 @@
1
+ # netaudio-lib
2
+
3
+ Python library for controlling Audinate Dante network audio devices.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install netaudio-lib
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from netaudio_lib import DanteBrowser
15
+
16
+ browser = DanteBrowser(mdns_timeout=5)
17
+ ```
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "netaudio-lib"
3
+ version = "0.0.1"
4
+ description = "Python library for controlling Audinate Dante network audio devices"
5
+ readme = "README.md"
6
+ license = "Unlicense"
7
+ requires-python = ">=3.9"
8
+ authors = [{ name = "Christopher Ritsen", email = "chris.ritsen@gmail.com" }]
9
+ keywords = ["audinate", "audio", "dante", "network"]
10
+ dependencies = [
11
+ "zeroconf>=0.38.3",
12
+ "ifaddr>=0.2.0",
13
+ "sqlitedict>=1.7.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/chris-ritsen/network-audio-controller"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = ["src/netaudio_lib"]
@@ -0,0 +1,21 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("netaudio-lib")
4
+
5
+ from netaudio_lib.dante.application import DanteApplication
6
+ from netaudio_lib.dante.browser import DanteBrowser
7
+ from netaudio_lib.dante.channel import DanteChannel
8
+ from netaudio_lib.dante.device import DanteDevice
9
+ from netaudio_lib.dante.events import DanteEvent, DanteEventDispatcher, EventType
10
+ from netaudio_lib.dante.subscription import DanteSubscription
11
+
12
+ __all__ = [
13
+ "DanteApplication",
14
+ "DanteBrowser",
15
+ "DanteChannel",
16
+ "DanteDevice",
17
+ "DanteEvent",
18
+ "DanteEventDispatcher",
19
+ "EventType",
20
+ "DanteSubscription",
21
+ ]
@@ -0,0 +1,96 @@
1
+ import sys
2
+
3
+ import ifaddr
4
+
5
+ DEFAULT_MDNS_TIMEOUT = 5
6
+ DEFAULT_INTERFACE = None
7
+
8
+
9
+ def get_available_interfaces():
10
+ interfaces = []
11
+ adapters = ifaddr.get_adapters()
12
+
13
+ for adapter in adapters:
14
+ for ip in adapter.ips:
15
+ if isinstance(ip.ip, str):
16
+ interfaces.append((adapter.nice_name, ip.ip, ip.network_prefix))
17
+
18
+ return sorted(interfaces)
19
+
20
+
21
+ class AppSettings:
22
+ def __init__(self):
23
+ self._mdns_timeout: float = DEFAULT_MDNS_TIMEOUT
24
+ self.dump_payloads: bool = False
25
+ self.debug: bool = False
26
+ self.no_color: bool = False
27
+ self._interface: str = DEFAULT_INTERFACE
28
+ self._interface_ip: str = None
29
+ self.refresh: bool = False
30
+ self.socket_path: str = None
31
+
32
+ @property
33
+ def mdns_timeout(self) -> float:
34
+ return self._mdns_timeout
35
+
36
+ @mdns_timeout.setter
37
+ def mdns_timeout(self, value: float) -> None:
38
+ if value > 0:
39
+ self._mdns_timeout = value
40
+ else:
41
+ print(
42
+ f"Warning: mDNS timeout must be positive. Received {value}. Using default {DEFAULT_MDNS_TIMEOUT}s instead.",
43
+ file=sys.stderr,
44
+ )
45
+
46
+ self._mdns_timeout = DEFAULT_MDNS_TIMEOUT
47
+
48
+ @property
49
+ def interface(self) -> str:
50
+ return self._interface
51
+
52
+ @interface.setter
53
+ def interface(self, value: str) -> None:
54
+ self._interface = value
55
+ self._interface_ip = None
56
+
57
+ @property
58
+ def interface_ip(self) -> str:
59
+ if not self._interface:
60
+ return None
61
+
62
+ if self._interface_ip:
63
+ return self._interface_ip
64
+
65
+ adapters = ifaddr.get_adapters()
66
+
67
+ for adapter in adapters:
68
+ if adapter.nice_name == self._interface:
69
+ ipv4_addresses = [ip.ip for ip in adapter.ips if isinstance(ip.ip, str)]
70
+
71
+ if ipv4_addresses:
72
+ self._interface_ip = ipv4_addresses[0]
73
+
74
+ print(
75
+ f"Using IPv4 address {self._interface_ip} for interface {self._interface}",
76
+ file=sys.stderr,
77
+ )
78
+
79
+ return self._interface_ip
80
+
81
+ print(
82
+ f"No IPv4 address found for interface {self._interface}",
83
+ file=sys.stderr,
84
+ )
85
+
86
+ return None
87
+
88
+ print(
89
+ f"Warning: Could not find interface '{self._interface}'. Using default interface.",
90
+ file=sys.stderr,
91
+ )
92
+
93
+ return None
94
+
95
+
96
+ settings = AppSettings()
@@ -0,0 +1,73 @@
1
+ import os
2
+ import tempfile
3
+ import time
4
+ from typing import Any, Dict, Optional, Union
5
+
6
+ from sqlitedict import SqliteDict
7
+
8
+ DEFAULT_CACHE_TTL = 600
9
+ CACHE_FILENAME = "netaudio_mdns_cache.sqlite"
10
+
11
+
12
+ class MdnsCache:
13
+ def __init__(self, ttl: int = DEFAULT_CACHE_TTL, cache_dir: Optional[str] = None):
14
+ self.ttl = ttl
15
+ if cache_dir is None:
16
+ cache_dir = tempfile.gettempdir()
17
+
18
+ self.cache_file_path = os.path.join(cache_dir, CACHE_FILENAME)
19
+
20
+ os.makedirs(cache_dir, exist_ok=True)
21
+
22
+ self._db = SqliteDict(self.cache_file_path, autocommit=True)
23
+
24
+ def get(self, key: str) -> Optional[Dict[str, Any]]:
25
+ if key not in self._db:
26
+ return None
27
+
28
+ entry: Union[Dict[str, Any], None] = self._db.get(key)
29
+
30
+ if (
31
+ entry is None
32
+ or not isinstance(entry, dict)
33
+ or "last_seen" not in entry
34
+ or "data" not in entry
35
+ ):
36
+ self.delete(key)
37
+ return None
38
+
39
+ last_seen_timestamp = entry.get("last_seen", 0)
40
+ if not isinstance(last_seen_timestamp, (int, float)):
41
+ self.delete(key)
42
+ return None
43
+
44
+ if time.time() - last_seen_timestamp > self.ttl:
45
+ self.delete(key)
46
+ return None
47
+
48
+ return entry.get("data")
49
+
50
+ def set(self, key: str, value: Dict[str, Any]) -> None:
51
+ entry = {"data": value, "last_seen": time.time()}
52
+ self._db[key] = entry
53
+
54
+ def delete(self, key: str) -> None:
55
+ if key in self._db:
56
+ del self._db[key]
57
+
58
+ def clear(self) -> None:
59
+ self._db.clear()
60
+
61
+ def close(self) -> None:
62
+ if hasattr(self, "_db") and self._db is not None:
63
+ self._db.close()
64
+
65
+ def __del__(self):
66
+ self.close()
67
+
68
+ def __enter__(self):
69
+ return self
70
+
71
+ def __exit__(self, exc_type, exc_val, exc_tb):
72
+ self.close()
73
+ return False
@@ -0,0 +1,44 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ def get_runtime_dir() -> Path:
7
+ if sys.platform == "win32":
8
+ return Path(os.environ.get("TEMP", os.environ.get("TMP", "C:\\Temp")))
9
+
10
+ xdg = os.environ.get("XDG_RUNTIME_DIR")
11
+ if xdg:
12
+ return Path(xdg)
13
+
14
+ tmpdir = os.environ.get("TMPDIR")
15
+ if tmpdir:
16
+ return Path(tmpdir)
17
+
18
+ return Path("/tmp")
19
+
20
+
21
+ def get_socket_dir() -> Path:
22
+ from netaudio_lib.common.app_config import settings as app_settings
23
+
24
+ if app_settings.socket_path:
25
+ return Path(app_settings.socket_path).parent
26
+
27
+ runtime_dir = get_runtime_dir()
28
+ socket_dir = runtime_dir / "netaudio"
29
+ return socket_dir
30
+
31
+
32
+ def get_socket_path() -> Path:
33
+ from netaudio_lib.common.app_config import settings as app_settings
34
+
35
+ if app_settings.socket_path:
36
+ return Path(app_settings.socket_path)
37
+
38
+ return get_socket_dir() / "netaudio.sock"
39
+
40
+
41
+ def ensure_socket_dir() -> Path:
42
+ socket_dir = get_socket_dir()
43
+ socket_dir.mkdir(parents=True, exist_ok=True)
44
+ return socket_dir
@@ -0,0 +1,2 @@
1
+ from netaudio_lib.daemon.client import get_devices_from_daemon
2
+ from netaudio_lib.daemon.server import NetaudioDaemon, run_daemon
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import logging
3
+ import pickle
4
+ import struct
5
+
6
+ from netaudio_lib.common.socket_path import get_socket_path
7
+ from netaudio_lib.dante.device import DanteDevice
8
+
9
+ logger = logging.getLogger("netaudio")
10
+
11
+
12
+ async def get_devices_from_daemon() -> dict[str, DanteDevice] | None:
13
+ socket_path = get_socket_path()
14
+
15
+ if not socket_path.exists():
16
+ return None
17
+
18
+ try:
19
+ reader, writer = await asyncio.wait_for(
20
+ asyncio.open_unix_connection(str(socket_path)),
21
+ timeout=1.0,
22
+ )
23
+
24
+ writer.write(b'\x00')
25
+ await writer.drain()
26
+
27
+ length_data = await asyncio.wait_for(reader.readexactly(4), timeout=1.0)
28
+ length = struct.unpack(">I", length_data)[0]
29
+
30
+ data = await asyncio.wait_for(reader.readexactly(length), timeout=2.0)
31
+ devices = pickle.loads(data)
32
+
33
+ writer.close()
34
+ await writer.wait_closed()
35
+
36
+ logger.info(f"Daemon: {len(devices)} devices")
37
+ return devices
38
+
39
+ except FileNotFoundError:
40
+ return None
41
+ except ConnectionRefusedError:
42
+ return None
43
+ except asyncio.TimeoutError:
44
+ logger.warning("Daemon connection timed out")
45
+ return None
46
+ except Exception as e:
47
+ logger.debug(f"Daemon connection error: {e}")
48
+ return None
49
+
50
+
51
+ async def report_unresponsive_device(server_name: str) -> None:
52
+ socket_path = get_socket_path()
53
+
54
+ if not socket_path.exists():
55
+ return
56
+
57
+ try:
58
+ reader, writer = await asyncio.wait_for(
59
+ asyncio.open_unix_connection(str(socket_path)),
60
+ timeout=1.0,
61
+ )
62
+
63
+ name_bytes = server_name.encode("utf-8")
64
+ writer.write(b'\x01')
65
+ writer.write(struct.pack(">I", len(name_bytes)))
66
+ writer.write(name_bytes)
67
+ await writer.drain()
68
+
69
+ writer.close()
70
+ await writer.wait_closed()
71
+
72
+ except Exception as e:
73
+ logger.debug(f"Failed to report dead device: {e}")