librqbit_upnp_serve/
lib.rs1use std::{io::Write, time::Duration};
2
3use anyhow::Context;
4use gethostname::gethostname;
5use librqbit_sha1_wrapper::ISha1;
6use services::content_directory::ContentDirectoryBrowseProvider;
7use ssdp::SsdpRunner;
8
9use tokio_util::sync::CancellationToken;
10use tracing::{debug, info};
11
12mod constants;
13mod http_server;
14pub mod services;
15mod ssdp;
16pub mod state;
17mod subscriptions;
18mod templates;
19
20pub struct UpnpServerOptions {
21 pub friendly_name: String,
22 pub http_listen_port: u16,
23 pub http_prefix: String,
24 pub browse_provider: Box<dyn ContentDirectoryBrowseProvider>,
25 pub cancellation_token: CancellationToken,
26}
27
28pub struct UpnpServer {
29 axum_router: Option<axum::Router>,
30 ssdp_runner: SsdpRunner,
31}
32
33fn create_usn(opts: &UpnpServerOptions) -> anyhow::Result<String> {
34 let mut buf = Vec::new();
35
36 buf.write_all(gethostname().as_encoded_bytes())?;
37 write!(
38 &mut buf,
39 "{}{}{}",
40 opts.friendly_name, opts.http_listen_port, opts.http_prefix
41 )?;
42
43 let mut sha1 = librqbit_sha1_wrapper::Sha1::new();
44 sha1.update(&buf);
45
46 let hash = sha1.finish();
47 let uuid = uuid::Builder::from_slice(&hash[..16])
48 .context("error generating UUID")?
49 .into_uuid();
50 Ok(format!("uuid:{}", uuid))
51}
52
53impl UpnpServer {
54 pub async fn new(opts: UpnpServerOptions) -> anyhow::Result<Self> {
55 let usn = create_usn(&opts).context("error generating USN")?;
56
57 let description_http_location = {
58 let port = opts.http_listen_port;
59 let http_prefix = &opts.http_prefix;
60 let surl = format!("http://0.0.0.0:{port}{http_prefix}/description.xml");
61 url::Url::parse(&surl)
62 .context(surl)
63 .context("error parsing url")?
64 };
65
66 info!(
67 location = %description_http_location,
68 "starting UPnP/SSDP announcer for MediaServer"
69 );
70 let ssdp_runner = crate::ssdp::SsdpRunner::new(ssdp::SsdpRunnerOptions {
71 usn: usn.clone(),
72 description_http_location,
73 server_string: "Linux/3.4 UPnP/1.0 rqbit/1".to_owned(),
74 notify_interval: Duration::from_secs(60),
75 shutdown: opts.cancellation_token.clone(),
76 })
77 .await
78 .context("error initializing SsdpRunner")?;
79
80 let router = crate::http_server::make_router(
81 opts.friendly_name,
82 opts.http_prefix,
83 usn,
84 opts.browse_provider,
85 opts.cancellation_token,
86 )?;
87
88 Ok(Self {
89 axum_router: Some(router),
90 ssdp_runner,
91 })
92 }
93
94 pub fn take_router(&mut self) -> anyhow::Result<axum::Router> {
95 self.axum_router
96 .take()
97 .context("programming error: router already taken")
98 }
99
100 pub async fn run_ssdp_forever(&self) -> anyhow::Result<()> {
101 debug!("starting SSDP");
102 self.ssdp_runner
103 .run_forever()
104 .await
105 .context("error running SSDP loop")
106 }
107}