librqbit_upnp_serve/
lib.rs

1use 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}