1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Internal implementation detail of [`sonos-sdk`](https://crates.io/crates/sonos-sdk). Not intended for direct use.
//!
//! Generic UPnP callback server for receiving event notifications.
//!
//! This crate provides a lightweight HTTP server for handling UPnP NOTIFY requests.
//! It is designed to be generic and has no knowledge of device-specific protocols.
//!
//! # Overview
//!
//! The callback server consists of three main components:
//!
//! - [`CallbackServer`]: HTTP server that binds to a local port and receives incoming
//! UPnP event notifications via HTTP POST requests.
//! - [`EventRouter`]: Routes incoming events based on subscription IDs to registered
//! handlers via channels.
//! - [`NotificationPayload`]: Generic data structure containing subscription ID and
//! raw XML event body.
//!
//! # Architecture
//!
//! The callback server is designed to be a thin HTTP layer that:
//!
//! 1. Binds to an available port in a specified range
//! 2. Validates incoming UPnP NOTIFY requests
//! 3. Extracts subscription IDs and event XML
//! 4. Routes events to registered handlers via channels
//!
//! All device-specific logic (speaker IDs, service types, event parsing) should be
//! handled by the consuming crate through an adapter layer.
//!
//! # Example: Basic Usage
//!
//! ```no_run
//! use callback_server::{CallbackServer, NotificationPayload};
//! use tokio::sync::mpsc;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), String> {
//! // Create a channel for receiving notifications
//! let (tx, mut rx) = mpsc::unbounded_channel::<NotificationPayload>();
//!
//! // Create and start the callback server
//! let server = CallbackServer::new((3400, 3500), tx).await?;
//!
//! println!("Callback server listening at: {}", server.base_url());
//!
//! // Register a subscription
//! server.router().register("uuid:subscription-123".to_string()).await;
//!
//! // Handle incoming notifications
//! tokio::spawn(async move {
//! while let Some(notification) = rx.recv().await {
//! println!("Received event for subscription: {}", notification.subscription_id);
//! println!("Event XML: {}", notification.event_xml);
//! }
//! });
//!
//! // Server runs until shutdown is called
//! // server.shutdown().await?;
//!
//! Ok(())
//! }
//! ```
//!
//! # Example: With Adapter Layer
//!
//! Device-specific crates should create an adapter layer that wraps the generic
//! types and adds domain-specific context:
//!
//! ```no_run
//! use callback_server::{CallbackServer, NotificationPayload};
//! use tokio::sync::mpsc;
//! use std::collections::HashMap;
//! use std::sync::Arc;
//! use tokio::sync::RwLock;
//!
//! // Device-specific event with additional context
//! #[derive(Debug, Clone)]
//! struct DeviceEvent {
//! subscription_id: String,
//! device_id: String,
//! service_type: String,
//! event_xml: String,
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<(), String> {
//! // Create channels
//! let (notification_tx, mut notification_rx) = mpsc::unbounded_channel::<NotificationPayload>();
//! let (device_event_tx, mut device_event_rx) = mpsc::unbounded_channel::<DeviceEvent>();
//!
//! // Create callback server
//! let server = CallbackServer::new((3400, 3500), notification_tx).await?;
//!
//! // Maintain mapping from subscription ID to device context
//! let subscription_map: Arc<RwLock<HashMap<String, (String, String)>>> =
//! Arc::new(RwLock::new(HashMap::new()));
//!
//! // Spawn adapter task to add device-specific context
//! let map_clone = subscription_map.clone();
//! tokio::spawn(async move {
//! while let Some(notification) = notification_rx.recv().await {
//! let map = map_clone.read().await;
//! if let Some((device_id, service_type)) = map.get(¬ification.subscription_id) {
//! let device_event = DeviceEvent {
//! subscription_id: notification.subscription_id,
//! device_id: device_id.clone(),
//! service_type: service_type.clone(),
//! event_xml: notification.event_xml,
//! };
//! let _ = device_event_tx.send(device_event);
//! }
//! }
//! });
//!
//! // Register subscription with device context
//! let sub_id = "uuid:subscription-123".to_string();
//! server.router().register(sub_id.clone()).await;
//! subscription_map.write().await.insert(
//! sub_id,
//! ("device-001".to_string(), "AVTransport".to_string())
//! );
//!
//! // Process device-specific events
//! tokio::spawn(async move {
//! while let Some(event) = device_event_rx.recv().await {
//! println!("Device {} service {} event", event.device_id, event.service_type);
//! }
//! });
//!
//! Ok(())
//! }
//! ```
//!
//! # Private Workspace Crate
//!
//! This crate is intended for internal use within the workspace and is not published
//! to crates.io. It provides the foundation for device-specific event handling layers.
pub use ;
pub use ;
pub use CallbackServer;