Skip to main content

anni_provider/providers/
proxy.rs

1use crate::common::content_range_to_range;
2use crate::{AnniProvider, AudioInfo, AudioResourceReader, ProviderError, Range, ResourceReader};
3use async_trait::async_trait;
4use futures::TryStreamExt;
5use reqwest::Response;
6use std::borrow::Cow;
7use std::collections::HashSet;
8use std::num::NonZeroU8;
9
10pub struct ProxyBackend {
11    client: reqwest::Client,
12
13    url: String,
14    auth: String,
15}
16
17impl ProxyBackend {
18    pub fn new(url: String, auth: String) -> Self {
19        Self {
20            url,
21            auth,
22            client: reqwest::Client::new(),
23        }
24    }
25
26    pub async fn get(&self, path: &str, range: &Range) -> reqwest::Result<Response> {
27        let mut req = self
28            .client
29            .get(&format!("{}{}", self.url, path))
30            .header("Authorization", &self.auth);
31        if let Some(range) = range.to_range_header() {
32            req = req.header("Range", range);
33        }
34        let req = req.build().unwrap();
35        self.client.execute(req).await
36    }
37
38    pub async fn head(&self, path: &str) -> reqwest::Result<Response> {
39        let req = self
40            .client
41            .head(&format!("{}{}", self.url, path))
42            .header("Authorization", &self.auth)
43            .build()
44            .unwrap();
45        self.client.execute(req).await
46    }
47}
48
49#[async_trait]
50impl AnniProvider for ProxyBackend {
51    async fn albums(&self) -> Result<HashSet<Cow<str>>, ProviderError> {
52        let r = self
53            .get("/albums", &Range::FULL)
54            .await
55            .map_err(|e| ProviderError::RequestError(e))?;
56        Ok(r.json().await?)
57    }
58
59    async fn get_audio_info(
60        &self,
61        album_id: &str,
62        disc_id: NonZeroU8,
63        track_id: NonZeroU8,
64    ) -> Result<AudioInfo, ProviderError> {
65        let response = self
66            .head(&format!(
67                "/albums/{album_id}/discs/{disc_id}/tracks/{track_id}"
68            ))
69            .await
70            .map_err(|e| ProviderError::RequestError(e))?;
71        audio_info_from_response(&response)
72    }
73
74    async fn get_audio(
75        &self,
76        album_id: &str,
77        disc_id: NonZeroU8,
78        track_id: NonZeroU8,
79        range: Range,
80    ) -> Result<AudioResourceReader, ProviderError> {
81        let response = self
82            .get(
83                &format!("/{album_id}/{disc_id}/{track_id}?quality=lossless"),
84                &range,
85            )
86            .await
87            .map_err(|e| ProviderError::RequestError(e))?;
88        let info = audio_info_from_response(&response)?;
89
90        let range = response
91            .headers()
92            .get("Content-Range")
93            .map(|s| s.to_str().unwrap().to_string());
94        let body = response
95            .bytes_stream()
96            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
97            .into_async_read();
98        let body = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
99        Ok(AudioResourceReader {
100            info,
101            range: content_range_to_range(range.as_deref()),
102            reader: Box::pin(body),
103        })
104    }
105
106    async fn get_cover(
107        &self,
108        album_id: &str,
109        disc_id: Option<NonZeroU8>,
110    ) -> Result<ResourceReader, ProviderError> {
111        let path = match disc_id {
112            Some(disc_id) => format!("/{album_id}/{disc_id}/cover"),
113            None => format!("/{album_id}/cover"),
114        };
115        let resp = self
116            .get(&path, &Range::FULL)
117            .await
118            .map_err(|e| ProviderError::RequestError(e))?;
119        let body = resp
120            .bytes_stream()
121            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
122            .into_async_read();
123        let body = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
124        Ok(Box::pin(body))
125    }
126
127    async fn reload(&mut self) -> Result<(), ProviderError> {
128        // proxy provider does not need to be reloaded
129        Ok(())
130    }
131}
132
133fn audio_info_from_response(response: &Response) -> Result<AudioInfo, ProviderError> {
134    let original_size = match response.headers().get("x-origin-size") {
135        Some(s) => s.to_str().unwrap_or("0"),
136        None => "0",
137    }
138    .to_string();
139    let duration = match response.headers().get("x-duration-seconds") {
140        Some(s) => s.to_str().unwrap_or("0"),
141        None => "0",
142    }
143    .to_string();
144    let extension = match response.headers().get("Content-Type") {
145        Some(content_type) => {
146            let content_type = content_type.to_str().unwrap_or("audio/flac");
147            content_type
148                .strip_prefix("audio/")
149                .unwrap_or("flac")
150                .to_string()
151        }
152        None => "flac".to_string(),
153    };
154    Ok(AudioInfo {
155        extension,
156        size: original_size
157            .parse()
158            .map_err(|_| ProviderError::GeneralError)?,
159        duration: duration.parse().map_err(|_| ProviderError::GeneralError)?,
160    })
161}